mirror of
https://github.com/esphome/esphome.git
synced 2025-01-18 20:10:55 +00:00
Merge branch 'v1530l0x-timing-budget' of https://github.com/lastradanet/esphome into v1530l0x-timing-budget
This commit is contained in:
commit
12512d763e
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@ -46,7 +46,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@v6.10.0
|
||||
uses: docker/build-push-action@v6.12.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@ -72,7 +72,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@v6.10.0
|
||||
uses: docker/build-push-action@v6.12.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
4
.github/workflows/ci-docker.yml
vendored
4
.github/workflows/ci-docker.yml
vendored
@ -46,9 +46,9 @@ jobs:
|
||||
with:
|
||||
python-version: "3.9"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.7.1
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Set TAG
|
||||
run: |
|
||||
|
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -13,6 +13,7 @@ on:
|
||||
- ".github/workflows/ci.yml"
|
||||
- "!.yamllint"
|
||||
- "!.github/dependabot.yml"
|
||||
- "!docker/**"
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
|
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@ -65,7 +65,7 @@ jobs:
|
||||
pip3 install build
|
||||
python3 -m build
|
||||
- name: Publish
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.2
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.3
|
||||
|
||||
deploy-docker:
|
||||
name: Build ESPHome ${{ matrix.platform }}
|
||||
@ -90,10 +90,10 @@ jobs:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.7.1
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
- name: Set up QEMU
|
||||
if: matrix.platform != 'linux/amd64'
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@v3.3.0
|
||||
@ -141,7 +141,7 @@ jobs:
|
||||
echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
with:
|
||||
name: digests-${{ steps.sanitize.outputs.name }}
|
||||
path: /tmp/digests
|
||||
@ -184,7 +184,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.7.1
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
|
2
.github/workflows/sync-device-classes.yml
vendored
2
.github/workflows/sync-device-classes.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
||||
python ./script/sync-device_class.py
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@v7.0.5
|
||||
uses: peter-evans/create-pull-request@v7.0.6
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@nabucasa.com>
|
||||
|
@ -11,14 +11,6 @@ repos:
|
||||
args: [--fix]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 24.4.2
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
- --safe
|
||||
- --quiet
|
||||
files: ^((esphome|script|tests)/.+)?[^/]+\.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
@ -53,6 +45,6 @@ repos:
|
||||
hooks:
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: script/run-in-env.sh pylint
|
||||
language: script
|
||||
entry: python script/run-in-env pylint
|
||||
language: system
|
||||
types: [python]
|
||||
|
@ -131,6 +131,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
|
||||
esphome/components/ens160_i2c/* @latonita
|
||||
esphome/components/ens160_spi/* @latonita
|
||||
esphome/components/ens210/* @itn3rd77
|
||||
esphome/components/es7210/* @kahrendt
|
||||
esphome/components/es8311/* @kahrendt @kroimon
|
||||
esphome/components/esp32/* @esphome/core
|
||||
esphome/components/esp32_ble/* @Rapsssito @jesserockz
|
||||
@ -302,7 +303,7 @@ esphome/components/noblex/* @AGalfra
|
||||
esphome/components/npi19/* @bakerkj
|
||||
esphome/components/number/* @esphome/core
|
||||
esphome/components/one_wire/* @ssieb
|
||||
esphome/components/online_image/* @guillempages
|
||||
esphome/components/online_image/* @clydebarrow @guillempages
|
||||
esphome/components/opentherm/* @olegtarasov
|
||||
esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
@ -338,7 +339,6 @@ esphome/components/radon_eye_rd200/* @jeffeb3
|
||||
esphome/components/rc522/* @glmnet
|
||||
esphome/components/rc522_i2c/* @glmnet
|
||||
esphome/components/rc522_spi/* @glmnet
|
||||
esphome/components/resistance_sampler/* @jesserockz
|
||||
esphome/components/restart/* @esphome/core
|
||||
esphome/components/rf_bridge/* @jesserockz
|
||||
esphome/components/rgbct/* @jesserockz
|
||||
@ -355,6 +355,7 @@ esphome/components/sdl/* @clydebarrow
|
||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
|
||||
esphome/components/sdp3x/* @Azimath
|
||||
esphome/components/seeed_mr24hpc1/* @limengdu
|
||||
esphome/components/seeed_mr60bha2/* @limengdu
|
||||
esphome/components/seeed_mr60fda2/* @limengdu
|
||||
esphome/components/selec_meter/* @sourabhjaiswal
|
||||
esphome/components/select/* @esphome/core
|
||||
|
@ -1,6 +1,11 @@
|
||||
# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/)
|
||||
|
||||
[![ESPHome Logo](https://esphome.io/_images/logo-text.png)](https://esphome.io/)
|
||||
<a href="https://esphome.io/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://esphome.io/_static/logo-text-on-dark.svg", alt="ESPHome Logo">
|
||||
<img src="https://esphome.io/_static/logo-text-on-light.svg" alt="ESPHome Logo">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
**Documentation:** https://esphome.io/
|
||||
|
||||
|
@ -29,7 +29,7 @@ RUN \
|
||||
# Use pinned versions so that we get updates with build caching
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
python3-pip=23.0.1+dfsg-1 \
|
||||
python3-setuptools=66.1.1-1 \
|
||||
python3-setuptools=66.1.1-1+deb12u1 \
|
||||
python3-venv=3.11.2-1+b1 \
|
||||
python3-wheel=0.38.4-2 \
|
||||
iputils-ping=3:20221126-1+deb12u1 \
|
||||
|
@ -758,6 +758,14 @@ def parse_args(argv):
|
||||
options_parser.add_argument(
|
||||
"-q", "--quiet", help="Disable all ESPHome logs.", action="store_true"
|
||||
)
|
||||
options_parser.add_argument(
|
||||
"-l",
|
||||
"--log-level",
|
||||
help="Set the log level.",
|
||||
default=os.getenv("ESPHOME_LOG_LEVEL", "INFO"),
|
||||
action="store",
|
||||
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
)
|
||||
options_parser.add_argument(
|
||||
"--dashboard", help=argparse.SUPPRESS, action="store_true"
|
||||
)
|
||||
@ -987,11 +995,16 @@ def run_esphome(argv):
|
||||
args = parse_args(argv)
|
||||
CORE.dashboard = args.dashboard
|
||||
|
||||
# Override log level if verbose is set
|
||||
if args.verbose:
|
||||
args.log_level = "DEBUG"
|
||||
elif args.quiet:
|
||||
args.log_level = "CRITICAL"
|
||||
|
||||
setup_log(
|
||||
args.verbose,
|
||||
args.quiet,
|
||||
log_level=args.log_level,
|
||||
# Show timestamp for dashboard access logs
|
||||
args.command == "dashboard",
|
||||
include_timestamp=args.command == "dashboard",
|
||||
)
|
||||
|
||||
if args.command in PRE_CONFIG_ACTIONS:
|
||||
|
@ -1,11 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import pins
|
||||
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER
|
||||
|
||||
from esphome.core import CORE
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.const import PLATFORM_ESP8266
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
@ -15,6 +10,9 @@ from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
@ -102,11 +100,11 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
||||
6: adc1_channel_t.ADC1_CHANNEL_6,
|
||||
},
|
||||
VARIANT_ESP32H2: {
|
||||
0: adc1_channel_t.ADC1_CHANNEL_0,
|
||||
1: adc1_channel_t.ADC1_CHANNEL_1,
|
||||
2: adc1_channel_t.ADC1_CHANNEL_2,
|
||||
3: adc1_channel_t.ADC1_CHANNEL_3,
|
||||
4: adc1_channel_t.ADC1_CHANNEL_4,
|
||||
1: adc1_channel_t.ADC1_CHANNEL_0,
|
||||
2: adc1_channel_t.ADC1_CHANNEL_1,
|
||||
3: adc1_channel_t.ADC1_CHANNEL_2,
|
||||
4: adc1_channel_t.ADC1_CHANNEL_3,
|
||||
5: adc1_channel_t.ADC1_CHANNEL_4,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -3,13 +3,12 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/voltage_sampler/voltage_sampler.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_adc_cal.h>
|
||||
#include "driver/adc.h"
|
||||
#endif
|
||||
#endif // USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
@ -43,7 +42,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
this->channel1_ = ADC1_CHANNEL_MAX;
|
||||
}
|
||||
void set_autorange(bool autorange) { this->autorange_ = autorange; }
|
||||
#endif
|
||||
#endif // USE_ESP32
|
||||
|
||||
/// Update ADC values
|
||||
void update() override;
|
||||
@ -59,11 +58,11 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
std::string unique_id() override;
|
||||
#endif
|
||||
#endif // USE_ESP8266
|
||||
|
||||
#ifdef USE_RP2040
|
||||
void set_is_temperature() { this->is_temperature_ = true; }
|
||||
#endif
|
||||
#endif // USE_RP2040
|
||||
|
||||
protected:
|
||||
InternalGPIOPin *pin_;
|
||||
@ -72,7 +71,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
|
||||
#ifdef USE_RP2040
|
||||
bool is_temperature_{false};
|
||||
#endif
|
||||
#endif // USE_RP2040
|
||||
|
||||
#ifdef USE_ESP32
|
||||
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
|
||||
@ -83,8 +82,8 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {};
|
||||
#else
|
||||
esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {};
|
||||
#endif
|
||||
#endif
|
||||
#endif // ESP_IDF_VERSION_MAJOR
|
||||
#endif // USE_ESP32
|
||||
};
|
||||
|
||||
} // namespace adc
|
||||
|
24
esphome/components/adc/adc_sensor_common.cpp
Normal file
24
esphome/components/adc/adc_sensor_common.cpp
Normal file
@ -0,0 +1,24 @@
|
||||
#include "adc_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
static const char *const TAG = "adc.common";
|
||||
|
||||
void ADCSensor::update() {
|
||||
float value_v = this->sample();
|
||||
ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v);
|
||||
this->publish_state(value_v);
|
||||
}
|
||||
|
||||
void ADCSensor::set_sample_count(uint8_t sample_count) {
|
||||
if (sample_count != 0) {
|
||||
this->sample_count_ = sample_count;
|
||||
}
|
||||
}
|
||||
|
||||
float ADCSensor::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
} // namespace adc
|
||||
} // namespace esphome
|
@ -1,30 +1,13 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "adc_sensor.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#ifdef USE_ADC_SENSOR_VCC
|
||||
#include <Esp.h>
|
||||
ADC_MODE(ADC_VCC)
|
||||
#else
|
||||
#include <Arduino.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_RP2040
|
||||
#ifdef CYW43_USES_VSYS_PIN
|
||||
#include "pico/cyw43_arch.h"
|
||||
#endif
|
||||
#include <hardware/adc.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
static const char *const TAG = "adc";
|
||||
static const char *const TAG = "adc.esp32";
|
||||
|
||||
// 13-bit for S2, 12-bit for all other ESP32 variants
|
||||
#ifdef USE_ESP32
|
||||
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
|
||||
|
||||
#ifndef SOC_ADC_RTC_MAX_BITWIDTH
|
||||
@ -32,24 +15,15 @@ static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_widt
|
||||
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13;
|
||||
#else
|
||||
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12;
|
||||
#endif
|
||||
#endif
|
||||
#endif // USE_ESP32_VARIANT_ESP32S2
|
||||
#endif // SOC_ADC_RTC_MAX_BITWIDTH
|
||||
|
||||
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit)
|
||||
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit)
|
||||
#endif
|
||||
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;
|
||||
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;
|
||||
|
||||
#ifdef USE_RP2040
|
||||
extern "C"
|
||||
#endif
|
||||
void
|
||||
ADCSensor::setup() {
|
||||
void ADCSensor::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
|
||||
#if !defined(USE_ADC_SENSOR_VCC) && !defined(USE_RP2040)
|
||||
this->pin_->setup();
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32
|
||||
if (this->channel1_ != ADC1_CHANNEL_MAX) {
|
||||
adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
|
||||
if (!this->autorange_) {
|
||||
@ -61,7 +35,6 @@ extern "C"
|
||||
}
|
||||
}
|
||||
|
||||
// load characteristics for each attenuation
|
||||
for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) {
|
||||
auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2;
|
||||
auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS,
|
||||
@ -79,31 +52,10 @@ extern "C"
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_RP2040
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
adc_init();
|
||||
initialized = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGCONFIG(TAG, "ADC '%s' setup finished!", this->get_name().c_str());
|
||||
}
|
||||
|
||||
void ADCSensor::dump_config() {
|
||||
LOG_SENSOR("", "ADC Sensor", this);
|
||||
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
|
||||
#ifdef USE_ADC_SENSOR_VCC
|
||||
ESP_LOGCONFIG(TAG, " Pin: VCC");
|
||||
#else
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
#endif
|
||||
#endif // USE_ESP8266 || USE_LIBRETINY
|
||||
|
||||
#ifdef USE_ESP32
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
if (this->autorange_) {
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: auto");
|
||||
@ -125,55 +77,10 @@ void ADCSensor::dump_config() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_RP2040
|
||||
if (this->is_temperature_) {
|
||||
ESP_LOGCONFIG(TAG, " Pin: Temperature");
|
||||
} else {
|
||||
#ifdef USE_ADC_SENSOR_VCC
|
||||
ESP_LOGCONFIG(TAG, " Pin: VCC");
|
||||
#else
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
#endif // USE_ADC_SENSOR_VCC
|
||||
}
|
||||
#endif // USE_RP2040
|
||||
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
float ADCSensor::get_setup_priority() const { return setup_priority::DATA; }
|
||||
void ADCSensor::update() {
|
||||
float value_v = this->sample();
|
||||
ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v);
|
||||
this->publish_state(value_v);
|
||||
}
|
||||
|
||||
void ADCSensor::set_sample_count(uint8_t sample_count) {
|
||||
if (sample_count != 0) {
|
||||
this->sample_count_ = sample_count;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
float ADCSensor::sample() {
|
||||
uint32_t raw = 0;
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
#ifdef USE_ADC_SENSOR_VCC
|
||||
raw += ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance)
|
||||
#else
|
||||
raw += analogRead(this->pin_->get_pin()); // NOLINT
|
||||
#endif
|
||||
}
|
||||
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
|
||||
if (this->output_raw_) {
|
||||
return raw;
|
||||
}
|
||||
return raw / 1024.0f;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32
|
||||
float ADCSensor::sample() {
|
||||
if (!this->autorange_) {
|
||||
uint32_t sum = 0;
|
||||
@ -240,93 +147,17 @@ float ADCSensor::sample() {
|
||||
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]);
|
||||
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]);
|
||||
|
||||
// Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC)
|
||||
uint32_t c12 = std::min(raw12, ADC_HALF);
|
||||
uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF);
|
||||
uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF);
|
||||
uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF);
|
||||
// max theoretical csum value is 4096*4 = 16384
|
||||
uint32_t csum = c12 + c6 + c2 + c0;
|
||||
|
||||
// each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32
|
||||
uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0);
|
||||
return mv_scaled / (float) (csum * 1000U);
|
||||
}
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_RP2040
|
||||
float ADCSensor::sample() {
|
||||
if (this->is_temperature_) {
|
||||
adc_set_temp_sensor_enabled(true);
|
||||
delay(1);
|
||||
adc_select_input(4);
|
||||
uint32_t raw = 0;
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
raw += adc_read();
|
||||
}
|
||||
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
|
||||
adc_set_temp_sensor_enabled(false);
|
||||
if (this->output_raw_) {
|
||||
return raw;
|
||||
}
|
||||
return raw * 3.3f / 4096.0f;
|
||||
} else {
|
||||
uint8_t pin = this->pin_->get_pin();
|
||||
#ifdef CYW43_USES_VSYS_PIN
|
||||
if (pin == PICO_VSYS_PIN) {
|
||||
// Measuring VSYS on Raspberry Pico W needs to be wrapped with
|
||||
// `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in
|
||||
// https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and
|
||||
// VSYS ADC both share GPIO29
|
||||
cyw43_thread_enter();
|
||||
}
|
||||
#endif // CYW43_USES_VSYS_PIN
|
||||
|
||||
adc_gpio_init(pin);
|
||||
adc_select_input(pin - 26);
|
||||
|
||||
uint32_t raw = 0;
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
raw += adc_read();
|
||||
}
|
||||
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
|
||||
|
||||
#ifdef CYW43_USES_VSYS_PIN
|
||||
if (pin == PICO_VSYS_PIN) {
|
||||
cyw43_thread_exit();
|
||||
}
|
||||
#endif // CYW43_USES_VSYS_PIN
|
||||
|
||||
if (this->output_raw_) {
|
||||
return raw;
|
||||
}
|
||||
float coeff = pin == PICO_VSYS_PIN ? 3.0 : 1.0;
|
||||
return raw * 3.3f / 4096.0f * coeff;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
float ADCSensor::sample() {
|
||||
uint32_t raw = 0;
|
||||
if (this->output_raw_) {
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
raw += analogRead(this->pin_->get_pin()); // NOLINT
|
||||
}
|
||||
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
|
||||
return raw;
|
||||
}
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
raw += analogReadVoltage(this->pin_->get_pin()); // NOLINT
|
||||
}
|
||||
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
|
||||
return raw / 1000.0f;
|
||||
}
|
||||
#endif // USE_LIBRETINY
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
|
||||
#endif
|
||||
|
||||
} // namespace adc
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
58
esphome/components/adc/adc_sensor_esp8266.cpp
Normal file
58
esphome/components/adc/adc_sensor_esp8266.cpp
Normal file
@ -0,0 +1,58 @@
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include "adc_sensor.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ADC_SENSOR_VCC
|
||||
#include <Esp.h>
|
||||
ADC_MODE(ADC_VCC)
|
||||
#else
|
||||
#include <Arduino.h>
|
||||
#endif // USE_ADC_SENSOR_VCC
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
static const char *const TAG = "adc.esp8266";
|
||||
|
||||
void ADCSensor::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
|
||||
#ifndef USE_ADC_SENSOR_VCC
|
||||
this->pin_->setup();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ADCSensor::dump_config() {
|
||||
LOG_SENSOR("", "ADC Sensor", this);
|
||||
#ifdef USE_ADC_SENSOR_VCC
|
||||
ESP_LOGCONFIG(TAG, " Pin: VCC");
|
||||
#else
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
#endif // USE_ADC_SENSOR_VCC
|
||||
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
float ADCSensor::sample() {
|
||||
uint32_t raw = 0;
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
#ifdef USE_ADC_SENSOR_VCC
|
||||
raw += ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance)
|
||||
#else
|
||||
raw += analogRead(this->pin_->get_pin()); // NOLINT
|
||||
#endif // USE_ADC_SENSOR_VCC
|
||||
}
|
||||
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
|
||||
if (this->output_raw_) {
|
||||
return raw;
|
||||
}
|
||||
return raw / 1024.0f;
|
||||
}
|
||||
|
||||
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
|
||||
|
||||
} // namespace adc
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP8266
|
48
esphome/components/adc/adc_sensor_libretiny.cpp
Normal file
48
esphome/components/adc/adc_sensor_libretiny.cpp
Normal file
@ -0,0 +1,48 @@
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include "adc_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
static const char *const TAG = "adc.libretiny";
|
||||
|
||||
void ADCSensor::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
|
||||
#ifndef USE_ADC_SENSOR_VCC
|
||||
this->pin_->setup();
|
||||
#endif // !USE_ADC_SENSOR_VCC
|
||||
}
|
||||
|
||||
void ADCSensor::dump_config() {
|
||||
LOG_SENSOR("", "ADC Sensor", this);
|
||||
#ifdef USE_ADC_SENSOR_VCC
|
||||
ESP_LOGCONFIG(TAG, " Pin: VCC");
|
||||
#else // USE_ADC_SENSOR_VCC
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
#endif // USE_ADC_SENSOR_VCC
|
||||
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
float ADCSensor::sample() {
|
||||
uint32_t raw = 0;
|
||||
if (this->output_raw_) {
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
raw += analogRead(this->pin_->get_pin()); // NOLINT
|
||||
}
|
||||
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
|
||||
return raw;
|
||||
}
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
raw += analogReadVoltage(this->pin_->get_pin()); // NOLINT
|
||||
}
|
||||
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
|
||||
return raw / 1000.0f;
|
||||
}
|
||||
|
||||
} // namespace adc
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_LIBRETINY
|
93
esphome/components/adc/adc_sensor_rp2040.cpp
Normal file
93
esphome/components/adc/adc_sensor_rp2040.cpp
Normal file
@ -0,0 +1,93 @@
|
||||
#ifdef USE_RP2040
|
||||
|
||||
#include "adc_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef CYW43_USES_VSYS_PIN
|
||||
#include "pico/cyw43_arch.h"
|
||||
#endif // CYW43_USES_VSYS_PIN
|
||||
#include <hardware/adc.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
static const char *const TAG = "adc.rp2040";
|
||||
|
||||
void ADCSensor::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
adc_init();
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ADCSensor::dump_config() {
|
||||
LOG_SENSOR("", "ADC Sensor", this);
|
||||
if (this->is_temperature_) {
|
||||
ESP_LOGCONFIG(TAG, " Pin: Temperature");
|
||||
} else {
|
||||
#ifdef USE_ADC_SENSOR_VCC
|
||||
ESP_LOGCONFIG(TAG, " Pin: VCC");
|
||||
#else
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
#endif // USE_ADC_SENSOR_VCC
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
float ADCSensor::sample() {
|
||||
if (this->is_temperature_) {
|
||||
adc_set_temp_sensor_enabled(true);
|
||||
delay(1);
|
||||
adc_select_input(4);
|
||||
uint32_t raw = 0;
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
raw += adc_read();
|
||||
}
|
||||
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
|
||||
adc_set_temp_sensor_enabled(false);
|
||||
if (this->output_raw_) {
|
||||
return raw;
|
||||
}
|
||||
return raw * 3.3f / 4096.0f;
|
||||
}
|
||||
|
||||
uint8_t pin = this->pin_->get_pin();
|
||||
#ifdef CYW43_USES_VSYS_PIN
|
||||
if (pin == PICO_VSYS_PIN) {
|
||||
// Measuring VSYS on Raspberry Pico W needs to be wrapped with
|
||||
// `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in
|
||||
// https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and
|
||||
// VSYS ADC both share GPIO29
|
||||
cyw43_thread_enter();
|
||||
}
|
||||
#endif // CYW43_USES_VSYS_PIN
|
||||
|
||||
adc_gpio_init(pin);
|
||||
adc_select_input(pin - 26);
|
||||
|
||||
uint32_t raw = 0;
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
raw += adc_read();
|
||||
}
|
||||
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
|
||||
|
||||
#ifdef CYW43_USES_VSYS_PIN
|
||||
if (pin == PICO_VSYS_PIN) {
|
||||
cyw43_thread_exit();
|
||||
}
|
||||
#endif // CYW43_USES_VSYS_PIN
|
||||
|
||||
if (this->output_raw_) {
|
||||
return raw;
|
||||
}
|
||||
float coeff = pin == PICO_VSYS_PIN ? 3.0f : 1.0f;
|
||||
return raw * 3.3f / 4096.0f * coeff;
|
||||
}
|
||||
|
||||
} // namespace adc
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_RP2040
|
@ -1,28 +1,10 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation, core
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.components.image as espImage
|
||||
from esphome.components.image import (
|
||||
CONF_USE_TRANSPARENCY,
|
||||
LOCAL_SCHEMA,
|
||||
SOURCE_LOCAL,
|
||||
SOURCE_WEB,
|
||||
WEB_SCHEMA,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FILE,
|
||||
CONF_ID,
|
||||
CONF_PATH,
|
||||
CONF_RAW_DATA_ID,
|
||||
CONF_REPEAT,
|
||||
CONF_RESIZE,
|
||||
CONF_SOURCE,
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.const import CONF_ID, CONF_REPEAT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -30,6 +12,7 @@ AUTO_LOAD = ["image"]
|
||||
CODEOWNERS = ["@syndlex"]
|
||||
DEPENDENCIES = ["display"]
|
||||
MULTI_CONF = True
|
||||
MULTI_CONF_NO_DEFAULT = True
|
||||
|
||||
CONF_LOOP = "loop"
|
||||
CONF_START_FRAME = "start_frame"
|
||||
@ -51,72 +34,9 @@ SetFrameAction = animation_ns.class_(
|
||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
|
||||
TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
SOURCE_LOCAL: LOCAL_SCHEMA,
|
||||
SOURCE_WEB: WEB_SCHEMA,
|
||||
},
|
||||
key=CONF_SOURCE,
|
||||
)
|
||||
|
||||
|
||||
def _file_schema(value):
|
||||
if isinstance(value, str):
|
||||
return validate_file_shorthand(value)
|
||||
return TYPED_FILE_SCHEMA(value)
|
||||
|
||||
|
||||
FILE_SCHEMA = cv.Schema(_file_schema)
|
||||
|
||||
|
||||
def validate_file_shorthand(value):
|
||||
value = cv.string_strict(value)
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_WEB,
|
||||
CONF_URL: value,
|
||||
}
|
||||
)
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_LOCAL,
|
||||
CONF_PATH: value,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def validate_cross_dependencies(config):
|
||||
"""
|
||||
Validate fields whose possible values depend on other fields.
|
||||
For example, validate that explicitly transparent image types
|
||||
have "use_transparency" set to True.
|
||||
Also set the default value for those kind of dependent fields.
|
||||
"""
|
||||
image_type = config[CONF_TYPE]
|
||||
is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"]
|
||||
# If the use_transparency option was not specified, set the default depending on the image type
|
||||
if CONF_USE_TRANSPARENCY not in config:
|
||||
config[CONF_USE_TRANSPARENCY] = is_transparent_type
|
||||
|
||||
if is_transparent_type and not config[CONF_USE_TRANSPARENCY]:
|
||||
raise cv.Invalid(f"Image type {image_type} must always be transparent.")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
ANIMATION_SCHEMA = cv.Schema(
|
||||
cv.All(
|
||||
CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||
cv.Required(CONF_FILE): FILE_SCHEMA,
|
||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
|
||||
espImage.IMAGE_TYPE, upper=True
|
||||
),
|
||||
# Not setting default here on purpose; the default depends on the image type,
|
||||
# and thus will be set in the "validate_cross_dependencies" validator.
|
||||
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
|
||||
cv.Optional(CONF_LOOP): cv.All(
|
||||
{
|
||||
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
||||
@ -124,13 +44,9 @@ ANIMATION_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_REPEAT): cv.positive_int,
|
||||
}
|
||||
),
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
},
|
||||
validate_cross_dependencies,
|
||||
)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = ANIMATION_SCHEMA
|
||||
|
||||
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
@ -164,180 +80,26 @@ async def animation_action_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
from PIL import Image
|
||||
(
|
||||
prog_arr,
|
||||
width,
|
||||
height,
|
||||
image_type,
|
||||
trans_value,
|
||||
frame_count,
|
||||
) = await espImage.write_image(config, all_frames=True)
|
||||
|
||||
conf_file = config[CONF_FILE]
|
||||
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
|
||||
path = CORE.relative_config_path(conf_file[CONF_PATH])
|
||||
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
|
||||
path = espImage.compute_local_image_path(conf_file).as_posix()
|
||||
else:
|
||||
raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}")
|
||||
|
||||
try:
|
||||
image = Image.open(path)
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(f"Could not load image file {path}: {e}")
|
||||
|
||||
width, height = image.size
|
||||
frames = image.n_frames
|
||||
if CONF_RESIZE in config:
|
||||
new_width_max, new_height_max = config[CONF_RESIZE]
|
||||
ratio = min(new_width_max / width, new_height_max / height)
|
||||
width, height = int(width * ratio), int(height * ratio)
|
||||
elif width > 500 or height > 500:
|
||||
_LOGGER.warning(
|
||||
'The image "%s" you requested is very big. Please consider'
|
||||
" using the resize parameter.",
|
||||
path,
|
||||
)
|
||||
|
||||
transparent = config[CONF_USE_TRANSPARENCY]
|
||||
|
||||
if config[CONF_TYPE] == "GRAYSCALE":
|
||||
data = [0 for _ in range(height * width * frames)]
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
frame = image.convert("LA", dither=Image.Dither.NONE)
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
pixels = list(frame.getdata())
|
||||
if len(pixels) != height * width:
|
||||
raise core.EsphomeError(
|
||||
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
|
||||
)
|
||||
for pix, a in pixels:
|
||||
if transparent:
|
||||
if pix == 1:
|
||||
pix = 0
|
||||
if a < 0x80:
|
||||
pix = 1
|
||||
|
||||
data[pos] = pix
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "RGBA":
|
||||
data = [0 for _ in range(height * width * 4 * frames)]
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
frame = image.convert("RGBA")
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
pixels = list(frame.getdata())
|
||||
if len(pixels) != height * width:
|
||||
raise core.EsphomeError(
|
||||
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
|
||||
)
|
||||
for pix in pixels:
|
||||
data[pos] = pix[0]
|
||||
pos += 1
|
||||
data[pos] = pix[1]
|
||||
pos += 1
|
||||
data[pos] = pix[2]
|
||||
pos += 1
|
||||
data[pos] = pix[3]
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "RGB24":
|
||||
data = [0 for _ in range(height * width * 3 * frames)]
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
frame = image.convert("RGBA")
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
pixels = list(frame.getdata())
|
||||
if len(pixels) != height * width:
|
||||
raise core.EsphomeError(
|
||||
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
|
||||
)
|
||||
for r, g, b, a in pixels:
|
||||
if transparent:
|
||||
if r == 0 and g == 0 and b == 1:
|
||||
b = 0
|
||||
if a < 0x80:
|
||||
r = 0
|
||||
g = 0
|
||||
b = 1
|
||||
|
||||
data[pos] = r
|
||||
pos += 1
|
||||
data[pos] = g
|
||||
pos += 1
|
||||
data[pos] = b
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]:
|
||||
bytes_per_pixel = 3 if transparent else 2
|
||||
data = [0 for _ in range(height * width * bytes_per_pixel * frames)]
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
frame = image.convert("RGBA")
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
pixels = list(frame.getdata())
|
||||
if len(pixels) != height * width:
|
||||
raise core.EsphomeError(
|
||||
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
|
||||
)
|
||||
for r, g, b, a in pixels:
|
||||
R = r >> 3
|
||||
G = g >> 2
|
||||
B = b >> 3
|
||||
rgb = (R << 11) | (G << 5) | B
|
||||
data[pos] = rgb >> 8
|
||||
pos += 1
|
||||
data[pos] = rgb & 0xFF
|
||||
pos += 1
|
||||
if transparent:
|
||||
data[pos] = a
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
|
||||
width8 = ((width + 7) // 8) * 8
|
||||
data = [0 for _ in range((height * width8 // 8) * frames)]
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
if transparent:
|
||||
alpha = image.split()[-1]
|
||||
has_alpha = alpha.getextrema()[0] < 0xFF
|
||||
else:
|
||||
has_alpha = False
|
||||
frame = image.convert("1", dither=Image.Dither.NONE)
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
if transparent:
|
||||
alpha = alpha.resize([width, height])
|
||||
for x, y in [(i, j) for i in range(width) for j in range(height)]:
|
||||
if transparent and has_alpha:
|
||||
if not alpha.getpixel((x, y)):
|
||||
continue
|
||||
elif frame.getpixel((x, y)):
|
||||
continue
|
||||
|
||||
pos = x + y * width8 + (height * width8 * frameIndex)
|
||||
data[pos // 8] |= 0x80 >> (pos % 8)
|
||||
else:
|
||||
raise core.EsphomeError(
|
||||
f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}."
|
||||
)
|
||||
|
||||
rhs = [HexInt(x) for x in data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
prog_arr,
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
espImage.IMAGE_TYPE[config[CONF_TYPE]],
|
||||
frame_count,
|
||||
image_type,
|
||||
trans_value,
|
||||
)
|
||||
cg.add(var.set_transparency(transparent))
|
||||
if loop_config := config.get(CONF_LOOP):
|
||||
start = loop_config[CONF_START_FRAME]
|
||||
end = loop_config.get(CONF_END_FRAME, frames)
|
||||
end = loop_config.get(CONF_END_FRAME, frame_count)
|
||||
count = loop_config.get(CONF_REPEAT, -1)
|
||||
cg.add(var.set_loop(start, end, count))
|
||||
|
@ -6,8 +6,8 @@ namespace esphome {
|
||||
namespace animation {
|
||||
|
||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
|
||||
image::ImageType type)
|
||||
: Image(data_start, width, height, type),
|
||||
image::ImageType type, image::Transparency transparent)
|
||||
: Image(data_start, width, height, type, transparent),
|
||||
animation_data_start_(data_start),
|
||||
current_frame_(0),
|
||||
animation_frame_count_(animation_frame_count),
|
||||
|
@ -8,7 +8,8 @@ namespace animation {
|
||||
|
||||
class Animation : public image::Image {
|
||||
public:
|
||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type);
|
||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type,
|
||||
image::Transparency transparent);
|
||||
|
||||
uint32_t get_animation_frame_count() const;
|
||||
int get_current_frame() const;
|
||||
|
@ -25,8 +25,7 @@ void BLEClient::loop() {
|
||||
|
||||
void BLEClient::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BLE Client:");
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Auto-Connect: %s", TRUEFALSE(this->auto_connect_));
|
||||
BLEClientBase::dump_config();
|
||||
}
|
||||
|
||||
bool BLEClient::parse_device(const espbt::ESPBTDevice &device) {
|
||||
|
@ -13,6 +13,11 @@ namespace bluetooth_proxy {
|
||||
|
||||
static const char *const TAG = "bluetooth_proxy.connection";
|
||||
|
||||
void BluetoothConnection::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BLE Connection:");
|
||||
BLEClientBase::dump_config();
|
||||
}
|
||||
|
||||
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
|
||||
|
@ -11,6 +11,7 @@ class BluetoothProxy;
|
||||
|
||||
class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
||||
public:
|
||||
void dump_config() override;
|
||||
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||
|
@ -37,8 +37,9 @@ void ClimateIR::setup() {
|
||||
this->publish_state();
|
||||
});
|
||||
this->current_temperature = this->sensor_->state;
|
||||
} else
|
||||
} else {
|
||||
this->current_temperature = NAN;
|
||||
}
|
||||
// restore set points
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
|
@ -131,8 +131,9 @@ bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteRecei
|
||||
} else {
|
||||
parent->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
}
|
||||
} else
|
||||
} else {
|
||||
parent->mode = climate::CLIMATE_MODE_COOL;
|
||||
}
|
||||
|
||||
// Fan Speed
|
||||
if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || parent->mode == climate::CLIMATE_MODE_HEAT_COOL ||
|
||||
|
@ -298,6 +298,12 @@ void DalyBmsComponent::decode_data_(std::vector<uint8_t> data) {
|
||||
if (this->cell_16_voltage_sensor_) {
|
||||
this->cell_16_voltage_sensor_->publish_state((float) encode_uint16(it[5], it[6]) / 1000);
|
||||
}
|
||||
if (this->cell_17_voltage_sensor_) {
|
||||
this->cell_17_voltage_sensor_->publish_state((float) encode_uint16(it[7], it[8]) / 1000);
|
||||
}
|
||||
if (this->cell_18_voltage_sensor_) {
|
||||
this->cell_18_voltage_sensor_->publish_state((float) encode_uint16(it[9], it[10]) / 1000);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
@ -54,6 +54,8 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice {
|
||||
SUB_SENSOR(cell_14_voltage)
|
||||
SUB_SENSOR(cell_15_voltage)
|
||||
SUB_SENSOR(cell_16_voltage)
|
||||
SUB_SENSOR(cell_17_voltage)
|
||||
SUB_SENSOR(cell_18_voltage)
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
|
@ -52,6 +52,8 @@ CONF_CELL_13_VOLTAGE = "cell_13_voltage"
|
||||
CONF_CELL_14_VOLTAGE = "cell_14_voltage"
|
||||
CONF_CELL_15_VOLTAGE = "cell_15_voltage"
|
||||
CONF_CELL_16_VOLTAGE = "cell_16_voltage"
|
||||
CONF_CELL_17_VOLTAGE = "cell_17_voltage"
|
||||
CONF_CELL_18_VOLTAGE = "cell_18_voltage"
|
||||
ICON_CURRENT_DC = "mdi:current-dc"
|
||||
ICON_BATTERY_OUTLINE = "mdi:battery-outline"
|
||||
ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up"
|
||||
@ -92,6 +94,8 @@ TYPES = [
|
||||
CONF_CELL_14_VOLTAGE,
|
||||
CONF_CELL_15_VOLTAGE,
|
||||
CONF_CELL_16_VOLTAGE,
|
||||
CONF_CELL_17_VOLTAGE,
|
||||
CONF_CELL_18_VOLTAGE,
|
||||
]
|
||||
|
||||
CELL_VOLTAGE_SCHEMA = sensor.sensor_schema(
|
||||
@ -212,6 +216,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_CELL_14_VOLTAGE): CELL_VOLTAGE_SCHEMA,
|
||||
cv.Optional(CONF_CELL_15_VOLTAGE): CELL_VOLTAGE_SCHEMA,
|
||||
cv.Optional(CONF_CELL_16_VOLTAGE): CELL_VOLTAGE_SCHEMA,
|
||||
cv.Optional(CONF_CELL_17_VOLTAGE): CELL_VOLTAGE_SCHEMA,
|
||||
cv.Optional(CONF_CELL_18_VOLTAGE): CELL_VOLTAGE_SCHEMA,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
@ -50,6 +50,10 @@ void DebugComponent::dump_config() {
|
||||
this->reset_reason_->publish_state(get_reset_reason_());
|
||||
}
|
||||
#endif // USE_TEXT_SENSOR
|
||||
|
||||
#ifdef USE_ESP32
|
||||
this->log_partition_info_(); // Log partition information for ESP32
|
||||
#endif // USE_ESP32
|
||||
}
|
||||
|
||||
void DebugComponent::loop() {
|
||||
|
@ -55,6 +55,20 @@ class DebugComponent : public PollingComponent {
|
||||
#endif // USE_ESP32
|
||||
#endif // USE_SENSOR
|
||||
|
||||
#ifdef USE_ESP32
|
||||
/**
|
||||
* @brief Logs information about the device's partition table.
|
||||
*
|
||||
* This function iterates through the ESP32's partition table and logs details
|
||||
* about each partition, including its name, type, subtype, starting address,
|
||||
* and size. The information is useful for diagnosing issues related to flash
|
||||
* memory or verifying the partition configuration dynamically at runtime.
|
||||
*
|
||||
* Only available when compiled for ESP32 platforms.
|
||||
*/
|
||||
void log_partition_info_();
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
text_sensor::TextSensor *device_info_{nullptr};
|
||||
text_sensor::TextSensor *reset_reason_{nullptr};
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_system.h>
|
||||
#include <esp_chip_info.h>
|
||||
#include <esp_partition.h>
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
#include <esp32/rom/rtc.h>
|
||||
@ -28,8 +29,72 @@ namespace debug {
|
||||
|
||||
static const char *const TAG = "debug";
|
||||
|
||||
void DebugComponent::log_partition_info_() {
|
||||
ESP_LOGCONFIG(TAG, "Partition table:");
|
||||
ESP_LOGCONFIG(TAG, " %-12s %-4s %-8s %-10s %-10s", "Name", "Type", "Subtype", "Address", "Size");
|
||||
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
|
||||
while (it != NULL) {
|
||||
const esp_partition_t *partition = esp_partition_get(it);
|
||||
ESP_LOGCONFIG(TAG, " %-12s %-4d %-8d 0x%08X 0x%08X", partition->label, partition->type, partition->subtype,
|
||||
partition->address, partition->size);
|
||||
it = esp_partition_next(it);
|
||||
}
|
||||
esp_partition_iterator_release(it);
|
||||
}
|
||||
|
||||
std::string DebugComponent::get_reset_reason_() {
|
||||
std::string reset_reason;
|
||||
switch (esp_reset_reason()) {
|
||||
case ESP_RST_POWERON:
|
||||
reset_reason = "Reset due to power-on event";
|
||||
break;
|
||||
case ESP_RST_EXT:
|
||||
reset_reason = "Reset by external pin";
|
||||
break;
|
||||
case ESP_RST_SW:
|
||||
reset_reason = "Software reset via esp_restart";
|
||||
break;
|
||||
case ESP_RST_PANIC:
|
||||
reset_reason = "Software reset due to exception/panic";
|
||||
break;
|
||||
case ESP_RST_INT_WDT:
|
||||
reset_reason = "Reset (software or hardware) due to interrupt watchdog";
|
||||
break;
|
||||
case ESP_RST_TASK_WDT:
|
||||
reset_reason = "Reset due to task watchdog";
|
||||
break;
|
||||
case ESP_RST_WDT:
|
||||
reset_reason = "Reset due to other watchdogs";
|
||||
break;
|
||||
case ESP_RST_DEEPSLEEP:
|
||||
reset_reason = "Reset after exiting deep sleep mode";
|
||||
break;
|
||||
case ESP_RST_BROWNOUT:
|
||||
reset_reason = "Brownout reset (software or hardware)";
|
||||
break;
|
||||
case ESP_RST_SDIO:
|
||||
reset_reason = "Reset over SDIO";
|
||||
break;
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 4))
|
||||
case ESP_RST_USB:
|
||||
reset_reason = "Reset by USB peripheral";
|
||||
break;
|
||||
case ESP_RST_JTAG:
|
||||
reset_reason = "Reset by JTAG";
|
||||
break;
|
||||
case ESP_RST_EFUSE:
|
||||
reset_reason = "Reset due to efuse error";
|
||||
break;
|
||||
case ESP_RST_PWR_GLITCH:
|
||||
reset_reason = "Reset due to power glitch detected";
|
||||
break;
|
||||
case ESP_RST_CPU_LOCKUP:
|
||||
reset_reason = "Reset due to CPU lock up (double exception)";
|
||||
break;
|
||||
#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 4)
|
||||
#endif // USE_ESP32_VARIANT_ESP32
|
||||
default: // Includes ESP_RST_UNKNOWN
|
||||
switch (rtc_get_reset_reason(0)) {
|
||||
case POWERON_RESET:
|
||||
reset_reason = "Power On Reset";
|
||||
@ -134,6 +199,8 @@ std::string DebugComponent::get_reset_reason_() {
|
||||
default:
|
||||
reset_reason = "Unknown Reset Reason";
|
||||
}
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
|
||||
return reset_reason;
|
||||
}
|
||||
@ -223,6 +290,19 @@ void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
device_info += " Cores:" + to_string(info.cores);
|
||||
device_info += " Revision:" + to_string(info.revision);
|
||||
|
||||
// Framework detection
|
||||
device_info += "|Framework: ";
|
||||
#ifdef USE_ARDUINO
|
||||
ESP_LOGD(TAG, "Framework: Arduino");
|
||||
device_info += "Arduino";
|
||||
#elif defined(USE_ESP_IDF)
|
||||
ESP_LOGD(TAG, "Framework: ESP-IDF");
|
||||
device_info += "ESP-IDF";
|
||||
#else
|
||||
ESP_LOGW(TAG, "Framework: UNKNOWN");
|
||||
device_info += "UNKNOWN";
|
||||
#endif
|
||||
|
||||
ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version());
|
||||
device_info += "|ESP-IDF: ";
|
||||
device_info += esp_get_idf_version();
|
||||
@ -294,4 +374,4 @@ void DebugComponent::update_platform_() {
|
||||
|
||||
} // namespace debug
|
||||
} // namespace esphome
|
||||
#endif
|
||||
#endif // USE_ESP32
|
||||
|
@ -159,6 +159,15 @@ void DFPlayer::loop() {
|
||||
}
|
||||
break;
|
||||
case 9: // End byte
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
char byte_sequence[100];
|
||||
byte_sequence[0] = '\0';
|
||||
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
|
||||
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
|
||||
this->read_buffer_[i]);
|
||||
}
|
||||
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
|
||||
#endif
|
||||
if (byte != 0xEF) {
|
||||
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
@ -238,13 +247,17 @@ void DFPlayer::loop() {
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
break;
|
||||
case 0x3C:
|
||||
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
case 0x3D:
|
||||
ESP_LOGV(TAG, "Playback finished");
|
||||
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
default:
|
||||
ESP_LOGV(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
||||
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
||||
}
|
||||
this->sent_cmd_ = 0;
|
||||
this->read_pos_ = 0;
|
||||
|
@ -118,8 +118,9 @@ std::unique_ptr<Command> CircularCommandQueue::dequeue() {
|
||||
if (front_ == rear_) {
|
||||
front_ = -1;
|
||||
rear_ = -1;
|
||||
} else
|
||||
} else {
|
||||
front_ = (front_ + 1) % COMMAND_QUEUE_SIZE;
|
||||
}
|
||||
|
||||
return dequeued_cmd;
|
||||
}
|
||||
|
@ -157,10 +157,11 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
if (bit == 0) {
|
||||
bit = 7;
|
||||
byte++;
|
||||
} else
|
||||
} else {
|
||||
bit--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!report_errors && error_code != 0)
|
||||
return false;
|
||||
|
||||
|
@ -39,6 +39,7 @@ DisplayOnPageChangeTrigger = display_ns.class_(
|
||||
|
||||
CONF_ON_PAGE_CHANGE = "on_page_change"
|
||||
CONF_SHOW_TEST_CARD = "show_test_card"
|
||||
CONF_UNSPECIFIED = "unspecified"
|
||||
|
||||
DISPLAY_ROTATIONS = {
|
||||
0: display_ns.DISPLAY_ROTATION_0_DEGREES,
|
||||
@ -55,16 +56,22 @@ def validate_rotation(value):
|
||||
return cv.enum(DISPLAY_ROTATIONS, int=True)(value)
|
||||
|
||||
|
||||
def validate_auto_clear(value):
|
||||
if value == CONF_UNSPECIFIED:
|
||||
return value
|
||||
return cv.boolean(value)
|
||||
|
||||
|
||||
BASIC_DISPLAY_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_LAMBDA): cv.lambda_,
|
||||
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
cv.Optional(CONF_PAGES): cv.All(
|
||||
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DisplayPage),
|
||||
@ -82,7 +89,9 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
|
||||
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
|
||||
): validate_auto_clear,
|
||||
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
|
||||
}
|
||||
)
|
||||
@ -92,8 +101,12 @@ async def setup_display_core_(var, config):
|
||||
if CONF_ROTATION in config:
|
||||
cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
|
||||
|
||||
if CONF_AUTO_CLEAR_ENABLED in config:
|
||||
cg.add(var.set_auto_clear(config[CONF_AUTO_CLEAR_ENABLED]))
|
||||
if auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED):
|
||||
# Default to true if pages or lambda is specified. Ideally this would be done during validation, but
|
||||
# the possible schemas are too complex to do this easily.
|
||||
if auto_clear == CONF_UNSPECIFIED:
|
||||
auto_clear = CONF_LAMBDA in config or CONF_PAGES in config
|
||||
cg.add(var.set_auto_clear(auto_clear))
|
||||
|
||||
if CONF_PAGES in config:
|
||||
pages = []
|
||||
|
@ -1,6 +1,6 @@
|
||||
#include "display.h"
|
||||
#include "display_color_utils.h"
|
||||
#include <utility>
|
||||
#include "display_color_utils.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@ -266,9 +266,10 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
|
||||
if (dymax < float(-dxmax) * tan_a) {
|
||||
upd_dxmax = ceil(float(dymax) / tan_a);
|
||||
hline_width = -dxmax - upd_dxmax + 1;
|
||||
} else
|
||||
} else {
|
||||
hline_width = 0;
|
||||
}
|
||||
}
|
||||
if (hline_width > 0)
|
||||
this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color);
|
||||
}
|
||||
@ -670,7 +671,7 @@ void Display::strftime(int x, int y, BaseFont *font, Color color, Color backgrou
|
||||
this->print(x, y, font, color, align, buffer, background);
|
||||
}
|
||||
void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) {
|
||||
this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time);
|
||||
this->strftime(x, y, font, color, COLOR_OFF, align, format, time);
|
||||
}
|
||||
void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) {
|
||||
this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time);
|
||||
|
@ -90,9 +90,10 @@ void Rect::info(const std::string &prefix) {
|
||||
if (this->is_set()) {
|
||||
ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
|
||||
this->y2());
|
||||
} else
|
||||
} else {
|
||||
ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace display
|
||||
} // namespace esphome
|
||||
|
67
esphome/components/es7210/__init__.py
Normal file
67
esphome/components/es7210/__init__.py
Normal file
@ -0,0 +1,67 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
es7210_ns = cg.esphome_ns.namespace("es7210")
|
||||
ES7210 = es7210_ns.class_("ES7210", cg.Component, i2c.I2CDevice)
|
||||
|
||||
|
||||
es7210_bits_per_sample = es7210_ns.enum("ES7210BitsPerSample")
|
||||
ES7210_BITS_PER_SAMPLE_ENUM = {
|
||||
16: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_16,
|
||||
24: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_24,
|
||||
32: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_32,
|
||||
}
|
||||
|
||||
|
||||
es7210_mic_gain = es7210_ns.enum("ES7210MicGain")
|
||||
ES7210_MIC_GAIN_ENUM = {
|
||||
"0DB": es7210_mic_gain.ES7210_MIC_GAIN_0DB,
|
||||
"3DB": es7210_mic_gain.ES7210_MIC_GAIN_3DB,
|
||||
"6DB": es7210_mic_gain.ES7210_MIC_GAIN_6DB,
|
||||
"9DB": es7210_mic_gain.ES7210_MIC_GAIN_9DB,
|
||||
"12DB": es7210_mic_gain.ES7210_MIC_GAIN_12DB,
|
||||
"15DB": es7210_mic_gain.ES7210_MIC_GAIN_15DB,
|
||||
"18DB": es7210_mic_gain.ES7210_MIC_GAIN_18DB,
|
||||
"21DB": es7210_mic_gain.ES7210_MIC_GAIN_21DB,
|
||||
"24DB": es7210_mic_gain.ES7210_MIC_GAIN_24DB,
|
||||
"27DB": es7210_mic_gain.ES7210_MIC_GAIN_27DB,
|
||||
"30DB": es7210_mic_gain.ES7210_MIC_GAIN_30DB,
|
||||
"33DB": es7210_mic_gain.ES7210_MIC_GAIN_33DB,
|
||||
"34.5DB": es7210_mic_gain.ES7210_MIC_GAIN_34_5DB,
|
||||
"36DB": es7210_mic_gain.ES7210_MIC_GAIN_36DB,
|
||||
"37.5DB": es7210_mic_gain.ES7210_MIC_GAIN_37_5DB,
|
||||
}
|
||||
|
||||
_validate_bits = cv.float_with_unit("bits", "bit")
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ES7210),
|
||||
cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All(
|
||||
_validate_bits, cv.enum(ES7210_BITS_PER_SAMPLE_ENUM)
|
||||
),
|
||||
cv.Optional(CONF_MIC_GAIN, default="24DB"): cv.enum(
|
||||
ES7210_MIC_GAIN_ENUM, upper=True
|
||||
),
|
||||
cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x40))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
|
||||
cg.add(var.set_mic_gain(config[CONF_MIC_GAIN]))
|
||||
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
|
201
esphome/components/es7210/es7210.cpp
Normal file
201
esphome/components/es7210/es7210.cpp
Normal file
@ -0,0 +1,201 @@
|
||||
#include "es7210.h"
|
||||
#include "es7210_const.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace es7210 {
|
||||
|
||||
static const char *const TAG = "es7210";
|
||||
|
||||
static const size_t MCLK_DIV_FRE = 256;
|
||||
|
||||
// Mark the component as failed; use only in setup
|
||||
#define ES7210_ERROR_FAILED(func) \
|
||||
if (!(func)) { \
|
||||
this->mark_failed(); \
|
||||
return; \
|
||||
}
|
||||
|
||||
// Return false; use outside of setup
|
||||
#define ES7210_ERROR_CHECK(func) \
|
||||
if (!(func)) { \
|
||||
return false; \
|
||||
}
|
||||
|
||||
void ES7210::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ES7210 ADC:");
|
||||
ESP_LOGCONFIG(TAG, " Bits Per Sample: %" PRIu8, this->bits_per_sample_);
|
||||
ESP_LOGCONFIG(TAG, " Sample Rate: %" PRIu32, this->sample_rate_);
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGCONFIG(TAG, " Failed to initialize!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ES7210::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up ES7210...");
|
||||
|
||||
// Software reset
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0xff));
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x32));
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_CLOCK_OFF_REG01, 0x3f));
|
||||
|
||||
// Set initialization time when device powers up
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_TIME_CONTROL0_REG09, 0x30));
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_TIME_CONTROL1_REG0A, 0x30));
|
||||
|
||||
// Configure HFP for all ADC channels
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC12_HPF2_REG23, 0x2a));
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC12_HPF1_REG22, 0x0a));
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF2_REG20, 0x0a));
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF1_REG21, 0x2a));
|
||||
|
||||
// Secondary I2S mode settings
|
||||
ES7210_ERROR_FAILED(this->es7210_update_reg_bit_(ES7210_MODE_CONFIG_REG08, 0x01, 0x00));
|
||||
|
||||
// Configure analog power
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_ANALOG_REG40, 0xC3));
|
||||
|
||||
// Set mic bias
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC12_BIAS_REG41, 0x70));
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC34_BIAS_REG42, 0x70));
|
||||
|
||||
// Configure I2S settings, sample rate, and microphone gains
|
||||
ES7210_ERROR_FAILED(this->configure_i2s_format_());
|
||||
ES7210_ERROR_FAILED(this->configure_sample_rate_());
|
||||
ES7210_ERROR_FAILED(this->configure_mic_gain_());
|
||||
|
||||
// Power on mics 1 through 4
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC1_POWER_REG47, 0x08));
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC2_POWER_REG48, 0x08));
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC3_POWER_REG49, 0x08));
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC4_POWER_REG4A, 0x08));
|
||||
|
||||
// Power down DLL
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_POWER_DOWN_REG06, 0x04));
|
||||
|
||||
// Power on MIC1-4 bias & ADC1-4 & PGA1-4 Power
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x0F));
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x0F));
|
||||
|
||||
// Enable device
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x71));
|
||||
ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x41));
|
||||
}
|
||||
|
||||
bool ES7210::configure_sample_rate_() {
|
||||
int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE;
|
||||
int coeff = -1;
|
||||
|
||||
for (int i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) {
|
||||
if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre)
|
||||
coeff = i;
|
||||
}
|
||||
|
||||
if (coeff >= 0) {
|
||||
// Set adc_div & doubler & dll
|
||||
uint8_t regv;
|
||||
ES7210_ERROR_CHECK(this->read_byte(ES7210_MAINCLK_REG02, ®v));
|
||||
regv = regv & 0x00;
|
||||
regv |= ES7210_COEFFICIENTS[coeff].adc_div;
|
||||
regv |= ES7210_COEFFICIENTS[coeff].doubler << 6;
|
||||
regv |= ES7210_COEFFICIENTS[coeff].dll << 7;
|
||||
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MAINCLK_REG02, regv));
|
||||
|
||||
// Set osr
|
||||
regv = ES7210_COEFFICIENTS[coeff].osr;
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_OSR_REG07, regv));
|
||||
// Set lrck
|
||||
regv = ES7210_COEFFICIENTS[coeff].lrck_h;
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_LRCK_DIVH_REG04, regv));
|
||||
regv = ES7210_COEFFICIENTS[coeff].lrck_l;
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_LRCK_DIVL_REG05, regv));
|
||||
} else {
|
||||
// Invalid sample frequency
|
||||
ESP_LOGE(TAG, "Invalid sample rate");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
bool ES7210::configure_mic_gain_() {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00);
|
||||
}
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0xff));
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0xff));
|
||||
|
||||
// Configure mic 1
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x10, 0x10));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x0f, this->mic_gain_));
|
||||
|
||||
// Configure mic 2
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x10, 0x10));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, this->mic_gain_));
|
||||
|
||||
// Configure mic 3
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, this->mic_gain_));
|
||||
|
||||
// Configure mic 4
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, this->mic_gain_));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ES7210::configure_i2s_format_() {
|
||||
// Configure bits per sample
|
||||
uint8_t reg_val = 0;
|
||||
switch (this->bits_per_sample_) {
|
||||
case ES7210_BITS_PER_SAMPLE_16:
|
||||
reg_val = 0x60;
|
||||
break;
|
||||
case ES7210_BITS_PER_SAMPLE_18:
|
||||
reg_val = 0x40;
|
||||
break;
|
||||
case ES7210_BITS_PER_SAMPLE_20:
|
||||
reg_val = 0x20;
|
||||
break;
|
||||
case ES7210_BITS_PER_SAMPLE_24:
|
||||
reg_val = 0x00;
|
||||
break;
|
||||
case ES7210_BITS_PER_SAMPLE_32:
|
||||
reg_val = 0x80;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE1_REG11, reg_val));
|
||||
|
||||
if (this->enable_tdm_) {
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE2_REG12, 0x02));
|
||||
} else {
|
||||
// Microphones 1 and 2 output on SDOUT1, microphones 3 and 4 output on SDOUT2
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE2_REG12, 0x00));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ES7210::es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data) {
|
||||
uint8_t regv;
|
||||
ES7210_ERROR_CHECK(this->read_byte(reg_addr, ®v));
|
||||
regv = (regv & (~update_bits)) | (update_bits & data);
|
||||
return this->write_byte(reg_addr, regv);
|
||||
}
|
||||
|
||||
} // namespace es7210
|
||||
} // namespace esphome
|
69
esphome/components/es7210/es7210.h
Normal file
69
esphome/components/es7210/es7210.h
Normal file
@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace es7210 {
|
||||
|
||||
enum ES7210BitsPerSample : uint8_t {
|
||||
ES7210_BITS_PER_SAMPLE_16 = 16,
|
||||
ES7210_BITS_PER_SAMPLE_18 = 18,
|
||||
ES7210_BITS_PER_SAMPLE_20 = 20,
|
||||
ES7210_BITS_PER_SAMPLE_24 = 24,
|
||||
ES7210_BITS_PER_SAMPLE_32 = 32,
|
||||
};
|
||||
|
||||
enum ES7210MicGain : uint8_t {
|
||||
ES7210_MIC_GAIN_0DB = 0,
|
||||
ES7210_MIC_GAIN_3DB,
|
||||
ES7210_MIC_GAIN_6DB,
|
||||
ES7210_MIC_GAIN_9DB,
|
||||
ES7210_MIC_GAIN_12DB,
|
||||
ES7210_MIC_GAIN_15DB,
|
||||
ES7210_MIC_GAIN_18DB,
|
||||
ES7210_MIC_GAIN_21DB,
|
||||
ES7210_MIC_GAIN_24DB,
|
||||
ES7210_MIC_GAIN_27DB,
|
||||
ES7210_MIC_GAIN_30DB,
|
||||
ES7210_MIC_GAIN_33DB,
|
||||
ES7210_MIC_GAIN_34_5DB,
|
||||
ES7210_MIC_GAIN_36DB,
|
||||
ES7210_MIC_GAIN_37_5DB,
|
||||
};
|
||||
|
||||
class ES7210 : public Component, public i2c::I2CDevice {
|
||||
/* Class for configuring an ES7210 ADC for microphone input.
|
||||
* Based on code from:
|
||||
* - https://github.com/espressif/esp-bsp/ (accessed 20241219)
|
||||
* - https://github.com/espressif/esp-adf/ (accessed 20241219)
|
||||
*/
|
||||
public:
|
||||
void setup() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void dump_config() override;
|
||||
|
||||
void set_bits_per_sample(ES7210BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
|
||||
void set_mic_gain(ES7210MicGain mic_gain) { this->mic_gain_ = mic_gain; }
|
||||
void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; }
|
||||
|
||||
protected:
|
||||
/// @brief Updates an I2C registry address by modifying the current state
|
||||
/// @param reg_addr I2C register address
|
||||
/// @param update_bits Mask of allowed bits to be modified
|
||||
/// @param data Bit values to be written
|
||||
/// @return True if successful, false otherwise
|
||||
bool es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data);
|
||||
|
||||
bool configure_i2s_format_();
|
||||
bool configure_mic_gain_();
|
||||
bool configure_sample_rate_();
|
||||
|
||||
bool enable_tdm_{false}; // TDM is unsupported in ESPHome as of version 2024.12
|
||||
ES7210MicGain mic_gain_;
|
||||
ES7210BitsPerSample bits_per_sample_;
|
||||
uint32_t sample_rate_;
|
||||
};
|
||||
|
||||
} // namespace es7210
|
||||
} // namespace esphome
|
126
esphome/components/es7210/es7210_const.h
Normal file
126
esphome/components/es7210/es7210_const.h
Normal file
@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
#include "es7210.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace es7210 {
|
||||
|
||||
// ES7210 register addresses
|
||||
static const uint8_t ES7210_RESET_REG00 = 0x00; /* Reset control */
|
||||
static const uint8_t ES7210_CLOCK_OFF_REG01 = 0x01; /* Used to turn off the ADC clock */
|
||||
static const uint8_t ES7210_MAINCLK_REG02 = 0x02; /* Set ADC clock frequency division */
|
||||
|
||||
static const uint8_t ES7210_MASTER_CLK_REG03 = 0x03; /* MCLK source $ SCLK division */
|
||||
static const uint8_t ES7210_LRCK_DIVH_REG04 = 0x04; /* lrck_divh */
|
||||
static const uint8_t ES7210_LRCK_DIVL_REG05 = 0x05; /* lrck_divl */
|
||||
static const uint8_t ES7210_POWER_DOWN_REG06 = 0x06; /* power down */
|
||||
static const uint8_t ES7210_OSR_REG07 = 0x07;
|
||||
static const uint8_t ES7210_MODE_CONFIG_REG08 = 0x08; /* Set primary/secondary & channels */
|
||||
static const uint8_t ES7210_TIME_CONTROL0_REG09 = 0x09; /* Set Chip intial state period*/
|
||||
static const uint8_t ES7210_TIME_CONTROL1_REG0A = 0x0A; /* Set Power up state period */
|
||||
static const uint8_t ES7210_SDP_INTERFACE1_REG11 = 0x11; /* Set sample & fmt */
|
||||
static const uint8_t ES7210_SDP_INTERFACE2_REG12 = 0x12; /* Pins state */
|
||||
static const uint8_t ES7210_ADC_AUTOMUTE_REG13 = 0x13; /* Set mute */
|
||||
static const uint8_t ES7210_ADC34_MUTERANGE_REG14 = 0x14; /* Set mute range */
|
||||
static const uint8_t ES7210_ADC12_MUTERANGE_REG15 = 0x15; /* Set mute range */
|
||||
static const uint8_t ES7210_ADC34_HPF2_REG20 = 0x20; /* HPF */
|
||||
static const uint8_t ES7210_ADC34_HPF1_REG21 = 0x21; /* HPF */
|
||||
static const uint8_t ES7210_ADC12_HPF1_REG22 = 0x22; /* HPF */
|
||||
static const uint8_t ES7210_ADC12_HPF2_REG23 = 0x23; /* HPF */
|
||||
static const uint8_t ES7210_ANALOG_REG40 = 0x40; /* ANALOG Power */
|
||||
static const uint8_t ES7210_MIC12_BIAS_REG41 = 0x41;
|
||||
static const uint8_t ES7210_MIC34_BIAS_REG42 = 0x42;
|
||||
static const uint8_t ES7210_MIC1_GAIN_REG43 = 0x43;
|
||||
static const uint8_t ES7210_MIC2_GAIN_REG44 = 0x44;
|
||||
static const uint8_t ES7210_MIC3_GAIN_REG45 = 0x45;
|
||||
static const uint8_t ES7210_MIC4_GAIN_REG46 = 0x46;
|
||||
static const uint8_t ES7210_MIC1_POWER_REG47 = 0x47;
|
||||
static const uint8_t ES7210_MIC2_POWER_REG48 = 0x48;
|
||||
static const uint8_t ES7210_MIC3_POWER_REG49 = 0x49;
|
||||
static const uint8_t ES7210_MIC4_POWER_REG4A = 0x4A;
|
||||
static const uint8_t ES7210_MIC12_POWER_REG4B = 0x4B; /* MICBias & ADC & PGA Power */
|
||||
static const uint8_t ES7210_MIC34_POWER_REG4C = 0x4C;
|
||||
|
||||
/*
|
||||
* Clock coefficient structer
|
||||
*/
|
||||
struct ES7210Coefficient {
|
||||
uint32_t mclk; // mclk frequency
|
||||
uint32_t lrclk;
|
||||
uint8_t ss_ds;
|
||||
uint8_t adc_div;
|
||||
uint8_t dll; // dll_bypass
|
||||
uint8_t doubler; // doubler_enable
|
||||
uint8_t osr; // adc osr
|
||||
uint8_t mclk_src; // sselect mclk source
|
||||
uint8_t lrck_h; // High 4 bits of lrck
|
||||
uint8_t lrck_l; // Low 8 bits of lrck
|
||||
};
|
||||
|
||||
/* Codec hifi mclk clock divider coefficients
|
||||
* MEMBER REG
|
||||
* mclk: 0x03
|
||||
* lrck: standard
|
||||
* ss_ds: --
|
||||
* adc_div: 0x02
|
||||
* dll: 0x06
|
||||
* doubler: 0x02
|
||||
* osr: 0x07
|
||||
* mclk_src: 0x03
|
||||
* lrckh: 0x04
|
||||
* lrckl: 0x05
|
||||
*/
|
||||
static const ES7210Coefficient ES7210_COEFFICIENTS[] = {
|
||||
// mclk lrck ss_ds adc_div dll doubler osr mclk_src lrckh lrckl
|
||||
/* 8k */
|
||||
{12288000, 8000, 0x00, 0x03, 0x01, 0x00, 0x20, 0x00, 0x06, 0x00},
|
||||
{16384000, 8000, 0x00, 0x04, 0x01, 0x00, 0x20, 0x00, 0x08, 0x00},
|
||||
{19200000, 8000, 0x00, 0x1e, 0x00, 0x01, 0x28, 0x00, 0x09, 0x60},
|
||||
{4096000, 8000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||
|
||||
/* 11.025k */
|
||||
{11289600, 11025, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
|
||||
|
||||
/* 12k */
|
||||
{12288000, 12000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
|
||||
{19200000, 12000, 0x00, 0x14, 0x00, 0x01, 0x28, 0x00, 0x06, 0x40},
|
||||
|
||||
/* 16k */
|
||||
{4096000, 16000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
|
||||
{19200000, 16000, 0x00, 0x0a, 0x00, 0x00, 0x1e, 0x00, 0x04, 0x80},
|
||||
{16384000, 16000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
|
||||
{12288000, 16000, 0x00, 0x03, 0x01, 0x01, 0x20, 0x00, 0x03, 0x00},
|
||||
|
||||
/* 22.05k */
|
||||
{11289600, 22050, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||
|
||||
/* 24k */
|
||||
{12288000, 24000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||
{19200000, 24000, 0x00, 0x0a, 0x00, 0x01, 0x28, 0x00, 0x03, 0x20},
|
||||
|
||||
/* 32k */
|
||||
{12288000, 32000, 0x00, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80},
|
||||
{16384000, 32000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||
{19200000, 32000, 0x00, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x02, 0x58},
|
||||
|
||||
/* 44.1k */
|
||||
{11289600, 44100, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
|
||||
|
||||
/* 48k */
|
||||
{12288000, 48000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
|
||||
{19200000, 48000, 0x00, 0x05, 0x00, 0x01, 0x28, 0x00, 0x01, 0x90},
|
||||
|
||||
/* 64k */
|
||||
{16384000, 64000, 0x01, 0x01, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
|
||||
{19200000, 64000, 0x00, 0x05, 0x00, 0x01, 0x1e, 0x00, 0x01, 0x2c},
|
||||
|
||||
/* 88.2k */
|
||||
{11289600, 88200, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
|
||||
|
||||
/* 96k */
|
||||
{12288000, 96000, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
|
||||
{19200000, 96000, 0x01, 0x05, 0x00, 0x01, 0x28, 0x00, 0x00, 0xc8},
|
||||
};
|
||||
|
||||
} // namespace es7210
|
||||
} // namespace esphome
|
@ -2,7 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.audio_dac import AudioDac
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_SAMPLE_RATE
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE
|
||||
|
||||
CODEOWNERS = ["@kroimon", "@kahrendt"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@ -10,7 +10,6 @@ DEPENDENCIES = ["i2c"]
|
||||
es8311_ns = cg.esphome_ns.namespace("es8311")
|
||||
ES8311 = es8311_ns.class_("ES8311", AudioDac, cg.Component, i2c.I2CDevice)
|
||||
|
||||
CONF_MIC_GAIN = "mic_gain"
|
||||
CONF_USE_MCLK = "use_mclk"
|
||||
CONF_USE_MICROPHONE = "use_microphone"
|
||||
|
||||
|
@ -602,6 +602,9 @@ async def to_code(config):
|
||||
cg.add_platformio_option(
|
||||
"platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"]
|
||||
)
|
||||
add_idf_sdkconfig_option(
|
||||
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
|
||||
)
|
||||
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False)
|
||||
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True)
|
||||
add_idf_sdkconfig_option(
|
||||
|
@ -1,4 +1,12 @@
|
||||
from .const import VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3
|
||||
from .const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
|
||||
ESP32_BASE_PINS = {
|
||||
"TX": 1,
|
||||
@ -1344,6 +1352,26 @@ done | sort
|
||||
"""
|
||||
|
||||
BOARDS = {
|
||||
"4d_systems_esp32s3_gen4_r8n16": {
|
||||
"name": "4D Systems GEN4-ESP32 16MB (ESP32S3-R8N16)",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"adafruit_camera_esp32s3": {
|
||||
"name": "Adafruit pyCamera S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"adafruit_feather_esp32c6": {
|
||||
"name": "Adafruit Feather ESP32-C6",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
},
|
||||
"adafruit_feather_esp32s2": {
|
||||
"name": "Adafruit Feather ESP32-S2",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
},
|
||||
"adafruit_feather_esp32s2_reversetft": {
|
||||
"name": "Adafruit Feather ESP32-S2 Reverse TFT",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
},
|
||||
"adafruit_feather_esp32s2_tft": {
|
||||
"name": "Adafruit Feather ESP32-S2 TFT",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
@ -1356,6 +1384,10 @@ BOARDS = {
|
||||
"name": "Adafruit Feather ESP32-S3 No PSRAM",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"adafruit_feather_esp32s3_reversetft": {
|
||||
"name": "Adafruit Feather ESP32-S3 Reverse TFT",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"adafruit_feather_esp32s3_tft": {
|
||||
"name": "Adafruit Feather ESP32-S3 TFT",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
@ -1376,10 +1408,18 @@ BOARDS = {
|
||||
"name": "Adafruit MagTag 2.9",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
},
|
||||
"adafruit_matrixportal_esp32s3": {
|
||||
"name": "Adafruit MatrixPortal ESP32-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"adafruit_metro_esp32s2": {
|
||||
"name": "Adafruit Metro ESP32-S2",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
},
|
||||
"adafruit_metro_esp32s3": {
|
||||
"name": "Adafruit Metro ESP32-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"adafruit_qtpy_esp32c3": {
|
||||
"name": "Adafruit QT Py ESP32-C3",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
@ -1392,10 +1432,18 @@ BOARDS = {
|
||||
"name": "Adafruit QT Py ESP32-S2",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
},
|
||||
"adafruit_qtpy_esp32s3_n4r2": {
|
||||
"name": "Adafruit QT Py ESP32-S3 (4M Flash 2M PSRAM)",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"adafruit_qtpy_esp32s3_nopsram": {
|
||||
"name": "Adafruit QT Py ESP32-S3 No PSRAM",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"adafruit_qualia_s3_rgb666": {
|
||||
"name": "Adafruit Qualia ESP32-S3 RGB666",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"airm2m_core_esp32c3": {
|
||||
"name": "AirM2M CORE ESP32C3",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
@ -1404,14 +1452,30 @@ BOARDS = {
|
||||
"name": "ALKS ESP32",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"arduino_nano_esp32": {
|
||||
"name": "Arduino Nano ESP32",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"atd147_s3": {
|
||||
"name": "ArtronShop ATD1.47-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"atmegazero_esp32s2": {
|
||||
"name": "EspinalLab ATMegaZero ESP32-S2",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
},
|
||||
"aventen_s3_sync": {
|
||||
"name": "Aventen S3 Sync",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"az-delivery-devkit-v4": {
|
||||
"name": "AZ-Delivery ESP-32 Dev Kit C V4",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"bee_data_logger": {
|
||||
"name": "Smart Bee Data Logger",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"bee_motion_mini": {
|
||||
"name": "Smart Bee Motion Mini",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
@ -1436,14 +1500,6 @@ BOARDS = {
|
||||
"name": "BPI-Leaf-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"briki_abc_esp32": {
|
||||
"name": "Briki ABC (MBC-WB) - ESP32",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"briki_mbc-wb_esp32": {
|
||||
"name": "Briki MBC-WB - ESP32",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"cnrs_aw2eth": {
|
||||
"name": "CNRS AW2ETH",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1496,18 +1552,38 @@ BOARDS = {
|
||||
"name": "DFRobot Beetle ESP32-C3",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
},
|
||||
"dfrobot_firebeetle2_esp32e": {
|
||||
"name": "DFRobot Firebeetle 2 ESP32-E",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"dfrobot_firebeetle2_esp32s3": {
|
||||
"name": "DFRobot Firebeetle 2 ESP32-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"dfrobot_romeo_esp32s3": {
|
||||
"name": "DFRobot Romeo ESP32-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"dpu_esp32": {
|
||||
"name": "TAMC DPU ESP32",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"edgebox-esp-100": {
|
||||
"name": "Seeed Studio Edgebox-ESP-100",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"esp320": {
|
||||
"name": "Electronic SweetPeas ESP320",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"esp32-c2-devkitm-1": {
|
||||
"name": "Espressif ESP32-C2-DevKitM-1",
|
||||
"variant": VARIANT_ESP32C2,
|
||||
},
|
||||
"esp32-c3-devkitc-02": {
|
||||
"name": "Espressif ESP32-C3-DevKitC-02",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
},
|
||||
"esp32-c3-devkitm-1": {
|
||||
"name": "Espressif ESP32-C3-DevKitM-1",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
@ -1516,6 +1592,14 @@ BOARDS = {
|
||||
"name": "Ai-Thinker ESP-C3-M1-I-Kit",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
},
|
||||
"esp32-c6-devkitc-1": {
|
||||
"name": "Espressif ESP32-C6-DevKitC-1",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
},
|
||||
"esp32-c6-devkitm-1": {
|
||||
"name": "Espressif ESP32-C6-DevKitM-1",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
},
|
||||
"esp32cam": {
|
||||
"name": "AI Thinker ESP32-CAM",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1544,6 +1628,14 @@ BOARDS = {
|
||||
"name": "OLIMEX ESP32-GATEWAY",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"esp32-h2-devkitm-1": {
|
||||
"name": "Espressif ESP32-H2-DevKit",
|
||||
"variant": VARIANT_ESP32H2,
|
||||
},
|
||||
"esp32-pico-devkitm-2": {
|
||||
"name": "Espressif ESP32-PICO-DevKitM-2",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"esp32-poe-iso": {
|
||||
"name": "OLIMEX ESP32-PoE-ISO",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1580,10 +1672,22 @@ BOARDS = {
|
||||
"name": "Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"esp32-s3-korvo-2": {
|
||||
"name": "Espressif ESP32-S3-Korvo-2",
|
||||
"esp32-s3-devkitm-1": {
|
||||
"name": "Espressif ESP32-S3-DevKitM-1",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"esp32s3_powerfeather": {
|
||||
"name": "ESP32-S3 PowerFeather",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"esp32s3usbotg": {
|
||||
"name": "Espressif ESP32-S3-USB-OTG",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"esp32-solo1": {
|
||||
"name": "Espressif Generic ESP32-solo1 4M Flash",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"esp32thing": {
|
||||
"name": "SparkFun ESP32 Thing",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1652,9 +1756,9 @@ BOARDS = {
|
||||
"name": "Heltec WiFi Kit 32",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"heltec_wifi_kit_32_v2": {
|
||||
"name": "Heltec WiFi Kit 32 (V2)",
|
||||
"variant": VARIANT_ESP32,
|
||||
"heltec_wifi_kit_32_V3": {
|
||||
"name": "Heltec WiFi Kit 32 (V3)",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"heltec_wifi_lora_32": {
|
||||
"name": "Heltec WiFi LoRa 32",
|
||||
@ -1664,6 +1768,10 @@ BOARDS = {
|
||||
"name": "Heltec WiFi LoRa 32 (V2)",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"heltec_wifi_lora_32_V3": {
|
||||
"name": "Heltec WiFi LoRa 32 (V3)",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"heltec_wireless_stick_lite": {
|
||||
"name": "Heltec Wireless Stick Lite",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1708,6 +1816,14 @@ BOARDS = {
|
||||
"name": "oddWires IoT-Bus Proteus",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"ioxesp32": {
|
||||
"name": "ArtronShop IOXESP32",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"ioxesp32ps": {
|
||||
"name": "ArtronShop IOXESP32PS",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"kb32-ft": {
|
||||
"name": "MakerAsia KB32-FT",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1720,10 +1836,26 @@ BOARDS = {
|
||||
"name": "Labplus mPython",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"lilka_v2": {
|
||||
"name": "Lilka v2",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"lilygo-t-display": {
|
||||
"name": "LilyGo T-Display",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"lilygo-t-display-s3": {
|
||||
"name": "LilyGo T-Display-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"lionbit": {
|
||||
"name": "Lion:Bit Dev Board",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"lionbits3": {
|
||||
"name": "Lion:Bit S3 STEM Dev Board",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"lolin32_lite": {
|
||||
"name": "WEMOS LOLIN32 Lite",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1752,10 +1884,18 @@ BOARDS = {
|
||||
"name": "WEMOS LOLIN S2 PICO",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
},
|
||||
"lolin_s3_mini": {
|
||||
"name": "WEMOS LOLIN S3 Mini",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"lolin_s3": {
|
||||
"name": "WEMOS LOLIN S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"lolin_s3_pro": {
|
||||
"name": "WEMOS LOLIN S3 PRO",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"lopy4": {
|
||||
"name": "Pycom LoPy4",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1768,10 +1908,18 @@ BOARDS = {
|
||||
"name": "M5Stack-ATOM",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"m5stack-atoms3": {
|
||||
"name": "M5Stack AtomS3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"m5stack-core2": {
|
||||
"name": "M5Stack Core2",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"m5stack-core-esp32-16M": {
|
||||
"name": "M5Stack Core ESP32 16M",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"m5stack-core-esp32": {
|
||||
"name": "M5Stack Core ESP32",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1780,6 +1928,10 @@ BOARDS = {
|
||||
"name": "M5Stack-Core Ink",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"m5stack-cores3": {
|
||||
"name": "M5Stack CoreS3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"m5stack-fire": {
|
||||
"name": "M5Stack FIRE",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1788,6 +1940,14 @@ BOARDS = {
|
||||
"name": "M5Stack GREY ESP32",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"m5stack_paper": {
|
||||
"name": "M5Stack Paper",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"m5stack-stamps3": {
|
||||
"name": "M5Stack StampS3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"m5stack-station": {
|
||||
"name": "M5Stack Station",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1796,6 +1956,10 @@ BOARDS = {
|
||||
"name": "M5Stack Timer CAM",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"m5stamp-pico": {
|
||||
"name": "M5Stamp-Pico",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"m5stick-c": {
|
||||
"name": "M5Stick-C",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1832,10 +1996,26 @@ BOARDS = {
|
||||
"name": "Deparment of Alchemy MiniMain ESP32-S2",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
},
|
||||
"motorgo_mini_1": {
|
||||
"name": "MotorGo Mini 1 (ESP32-S3)",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"namino_arancio": {
|
||||
"name": "Namino Arancio",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"namino_rosso": {
|
||||
"name": "Namino Rosso",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"nano32": {
|
||||
"name": "MakerAsia Nano32",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"nebulas3": {
|
||||
"name": "Kinetic Dynamics Nebula S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"nina_w10": {
|
||||
"name": "u-blox NINA-W10 series",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1896,10 +2076,22 @@ BOARDS = {
|
||||
"name": "Munich Labs RedPill ESP32-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"roboheart_hercules": {
|
||||
"name": "RoboHeart Hercules",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"seeed_xiao_esp32c3": {
|
||||
"name": "Seeed Studio XIAO ESP32C3",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
},
|
||||
"seeed_xiao_esp32s3": {
|
||||
"name": "Seeed Studio XIAO ESP32S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"sensebox_mcu_esp32s2": {
|
||||
"name": "senseBox MCU-S2 ESP32-S2",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
},
|
||||
"sensesiot_weizen": {
|
||||
"name": "LOGISENSES Senses Weizen",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -1912,6 +2104,10 @@ BOARDS = {
|
||||
"name": "S.ODI Ultra v1",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"sparkfun_esp32c6_thing_plus": {
|
||||
"name": "Sparkfun ESP32-C6 Thing Plus",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
},
|
||||
"sparkfun_esp32_iot_redboard": {
|
||||
"name": "SparkFun ESP32 IoT RedBoard",
|
||||
"variant": VARIANT_ESP32,
|
||||
@ -2004,6 +2200,10 @@ BOARDS = {
|
||||
"name": "Unexpected Maker FeatherS3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"um_nanos3": {
|
||||
"name": "Unexpected Maker NanoS3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"um_pros3": {
|
||||
"name": "Unexpected Maker PROS3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
@ -2040,6 +2240,14 @@ BOARDS = {
|
||||
"name": "uPesy ESP32 Wrover DevKit",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"valtrack_v4_mfw_esp32_c3": {
|
||||
"name": "Valetron Systems VALTRACK-V4MVF",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
},
|
||||
"valtrack_v4_vts_esp32_c3": {
|
||||
"name": "Valetron Systems VALTRACK-V4VTS",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
},
|
||||
"vintlabs-devkit-v1": {
|
||||
"name": "VintLabs ESP32 Devkit",
|
||||
"variant": VARIANT_ESP32,
|
||||
|
@ -58,7 +58,11 @@ uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
|
||||
#else
|
||||
uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
|
||||
#endif
|
||||
uint32_t arch_get_cpu_freq_hz() { return rtc_clk_apb_freq_get(); }
|
||||
uint32_t arch_get_cpu_freq_hz() {
|
||||
rtc_cpu_freq_config_t config;
|
||||
rtc_clk_cpu_freq_get_config(&config);
|
||||
return config.freq_mhz * 1000000U;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
@ -27,6 +27,9 @@ namespace esp32_ble {
|
||||
|
||||
static const char *const TAG = "esp32_ble";
|
||||
|
||||
static RAMAllocator<BLEEvent> EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
|
||||
|
||||
void ESP32BLE::setup() {
|
||||
global_ble = this;
|
||||
ESP_LOGCONFIG(TAG, "Setting up BLE...");
|
||||
@ -322,7 +325,8 @@ void ESP32BLE::loop() {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
delete ble_event; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
ble_event->~BLEEvent();
|
||||
EVENT_ALLOCATOR.deallocate(ble_event, 1);
|
||||
ble_event = this->ble_events_.pop();
|
||||
}
|
||||
if (this->advertising_ != nullptr) {
|
||||
@ -331,9 +335,14 @@ void ESP32BLE::loop() {
|
||||
}
|
||||
|
||||
void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
BLEEvent *new_event = new BLEEvent(event, param); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
|
||||
if (new_event == nullptr) {
|
||||
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
|
||||
return;
|
||||
}
|
||||
new (new_event) BLEEvent(event, param);
|
||||
global_ble->ble_events_.push(new_event);
|
||||
} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
} // NOLINT(clang-analyzer-unix.Malloc)
|
||||
|
||||
void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event);
|
||||
@ -344,9 +353,14 @@ void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap
|
||||
|
||||
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
||||
esp_ble_gatts_cb_param_t *param) {
|
||||
BLEEvent *new_event = new BLEEvent(event, gatts_if, param); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
|
||||
if (new_event == nullptr) {
|
||||
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
|
||||
return;
|
||||
}
|
||||
new (new_event) BLEEvent(event, gatts_if, param);
|
||||
global_ble->ble_events_.push(new_event);
|
||||
} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
} // NOLINT(clang-analyzer-unix.Malloc)
|
||||
|
||||
void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
||||
esp_ble_gatts_cb_param_t *param) {
|
||||
@ -358,9 +372,14 @@ void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if
|
||||
|
||||
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
BLEEvent *new_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
|
||||
if (new_event == nullptr) {
|
||||
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
|
||||
return;
|
||||
}
|
||||
new (new_event) BLEEvent(event, gattc_if, param);
|
||||
global_ble->ble_events_.push(new_event);
|
||||
} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
} // NOLINT(clang-analyzer-unix.Malloc)
|
||||
|
||||
void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
|
@ -83,7 +83,7 @@ esp_err_t BLEAdvertising::services_advertisement_() {
|
||||
esp_err_t err;
|
||||
|
||||
this->advertising_data_.set_scan_rsp = false;
|
||||
this->advertising_data_.include_name = true;
|
||||
this->advertising_data_.include_name = !this->scan_response_;
|
||||
this->advertising_data_.include_txpower = !this->scan_response_;
|
||||
err = esp_ble_gap_config_adv_data(&this->advertising_data_);
|
||||
if (err != ESP_OK) {
|
||||
|
@ -26,11 +26,11 @@ template<class T> class Queue {
|
||||
void push(T *element) {
|
||||
if (element == nullptr)
|
||||
return;
|
||||
if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) {
|
||||
// It is not called from main loop. Thus it won't block main thread.
|
||||
xSemaphoreTake(m_, portMAX_DELAY);
|
||||
q_.push(element);
|
||||
xSemaphoreGive(m_);
|
||||
}
|
||||
}
|
||||
|
||||
T *pop() {
|
||||
T *element = nullptr;
|
||||
|
@ -44,6 +44,50 @@ void BLEClientBase::loop() {
|
||||
|
||||
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
|
||||
|
||||
void BLEClientBase::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Auto-Connect: %s", TRUEFALSE(this->auto_connect_));
|
||||
std::string state_name;
|
||||
switch (this->state()) {
|
||||
case espbt::ClientState::INIT:
|
||||
state_name = "INIT";
|
||||
break;
|
||||
case espbt::ClientState::DISCONNECTING:
|
||||
state_name = "DISCONNECTING";
|
||||
break;
|
||||
case espbt::ClientState::IDLE:
|
||||
state_name = "IDLE";
|
||||
break;
|
||||
case espbt::ClientState::SEARCHING:
|
||||
state_name = "SEARCHING";
|
||||
break;
|
||||
case espbt::ClientState::DISCOVERED:
|
||||
state_name = "DISCOVERED";
|
||||
break;
|
||||
case espbt::ClientState::READY_TO_CONNECT:
|
||||
state_name = "READY_TO_CONNECT";
|
||||
break;
|
||||
case espbt::ClientState::CONNECTING:
|
||||
state_name = "CONNECTING";
|
||||
break;
|
||||
case espbt::ClientState::CONNECTED:
|
||||
state_name = "CONNECTED";
|
||||
break;
|
||||
case espbt::ClientState::ESTABLISHED:
|
||||
state_name = "ESTABLISHED";
|
||||
break;
|
||||
default:
|
||||
state_name = "UNKNOWN_STATE";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " State: %s", state_name.c_str());
|
||||
if (this->status_ == ESP_GATT_NO_RESOURCES) {
|
||||
ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config.");
|
||||
} else if (this->status_ != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, " Failed due to error code %d", this->status_);
|
||||
}
|
||||
}
|
||||
|
||||
bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
|
||||
if (!this->auto_connect_)
|
||||
return false;
|
||||
@ -129,6 +173,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
} else {
|
||||
ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->reg.app_id, param->reg.status);
|
||||
this->status_ = param->reg.status;
|
||||
this->mark_failed();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
float get_setup_priority() const override;
|
||||
void dump_config() override;
|
||||
|
||||
void run_later(std::function<void()> &&f); // NOLINT
|
||||
bool parse_device(const espbt::ESPBTDevice &device) override;
|
||||
@ -103,6 +104,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
bool paired_{false};
|
||||
espbt::ConnectionType connection_type_{espbt::ConnectionType::V1};
|
||||
std::vector<BLEService *> services_;
|
||||
esp_gatt_status_t status_{ESP_GATT_OK};
|
||||
|
||||
void log_event_(const char *name);
|
||||
};
|
||||
|
@ -58,7 +58,6 @@ void ESP32BLETracker::setup() {
|
||||
global_esp32_ble_tracker = this;
|
||||
this->scan_result_lock_ = xSemaphoreCreateMutex();
|
||||
this->scan_end_lock_ = xSemaphoreCreateMutex();
|
||||
this->scanner_idle_ = true;
|
||||
|
||||
#ifdef USE_OTA
|
||||
ota::get_global_ota_callback()->add_on_state_callback(
|
||||
@ -107,6 +106,15 @@ void ESP32BLETracker::loop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (connecting != connecting_ || discovered != discovered_ || searching != searching_ ||
|
||||
disconnecting != disconnecting_) {
|
||||
connecting_ = connecting;
|
||||
discovered_ = discovered;
|
||||
searching_ = searching;
|
||||
disconnecting_ = disconnecting;
|
||||
ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
|
||||
searching_, disconnecting_);
|
||||
}
|
||||
bool promote_to_connecting = discovered && !searching && !connecting;
|
||||
|
||||
if (!this->scanner_idle_) {
|
||||
@ -183,8 +191,9 @@ void ESP32BLETracker::loop() {
|
||||
}
|
||||
|
||||
if (this->scan_start_failed_ || this->scan_set_param_failed_) {
|
||||
if (this->scan_start_fail_count_ == 255) {
|
||||
ESP_LOGE(TAG, "ESP-IDF BLE scan could not restart after 255 attempts, rebooting to restore BLE stack...");
|
||||
if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
|
||||
ESP_LOGE(TAG, "ESP-IDF BLE scan could not restart after %d attempts, rebooting to restore BLE stack...",
|
||||
std::numeric_limits<uint8_t>::max());
|
||||
App.reboot();
|
||||
}
|
||||
if (xSemaphoreTake(this->scan_end_lock_, 0L)) {
|
||||
@ -282,6 +291,12 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
this->scan_params_.scan_interval = this->scan_interval_;
|
||||
this->scan_params_.scan_window = this->scan_window_;
|
||||
|
||||
// Start timeout before scan is started. Otherwise scan never starts if any error.
|
||||
this->set_timeout("scan", this->scan_duration_ * 2000, []() {
|
||||
ESP_LOGE(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack...");
|
||||
App.reboot();
|
||||
});
|
||||
|
||||
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gap_set_scan_params failed: %d", err);
|
||||
@ -293,11 +308,6 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
return;
|
||||
}
|
||||
this->scanner_idle_ = false;
|
||||
|
||||
this->set_timeout("scan", this->scan_duration_ * 2000, []() {
|
||||
ESP_LOGE(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack...");
|
||||
App.reboot();
|
||||
});
|
||||
}
|
||||
|
||||
void ESP32BLETracker::end_of_scan_() {
|
||||
@ -371,6 +381,7 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) {
|
||||
ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
|
||||
if (param.status == ESP_BT_STATUS_DONE) {
|
||||
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
|
||||
} else {
|
||||
@ -379,20 +390,25 @@ void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t:
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) {
|
||||
ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status);
|
||||
this->scan_start_failed_ = param.status;
|
||||
if (param.status == ESP_BT_STATUS_SUCCESS) {
|
||||
this->scan_start_fail_count_ = 0;
|
||||
} else {
|
||||
if (this->scan_start_fail_count_ != std::numeric_limits<uint8_t>::max()) {
|
||||
this->scan_start_fail_count_++;
|
||||
}
|
||||
xSemaphoreGive(this->scan_end_lock_);
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) {
|
||||
ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status);
|
||||
xSemaphoreGive(this->scan_end_lock_);
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) {
|
||||
ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt);
|
||||
if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
|
||||
if (xSemaphoreTake(this->scan_result_lock_, 0L)) {
|
||||
if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
|
||||
@ -663,7 +679,14 @@ void ESP32BLETracker::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Scan Interval: %.1f ms", this->scan_interval_ * 0.625f);
|
||||
ESP_LOGCONFIG(TAG, " Scan Window: %.1f ms", this->scan_window_ * 0.625f);
|
||||
ESP_LOGCONFIG(TAG, " Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE");
|
||||
ESP_LOGCONFIG(TAG, " Continuous Scanning: %s", this->scan_continuous_ ? "True" : "False");
|
||||
ESP_LOGCONFIG(TAG, " Continuous Scanning: %s", YESNO(this->scan_continuous_));
|
||||
ESP_LOGCONFIG(TAG, " Scanner Idle: %s", YESNO(this->scanner_idle_));
|
||||
ESP_LOGCONFIG(TAG, " Scan End: %s", YESNO(xSemaphoreGetMutexHolder(this->scan_end_lock_) == nullptr));
|
||||
ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
|
||||
searching_, disconnecting_);
|
||||
if (this->scan_start_fail_count_) {
|
||||
ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) {
|
||||
|
@ -178,7 +178,7 @@ class ESPBTClient : public ESPBTDeviceListener {
|
||||
int app_id;
|
||||
|
||||
protected:
|
||||
ClientState state_;
|
||||
ClientState state_{ClientState::INIT};
|
||||
};
|
||||
|
||||
class ESP32BLETracker : public Component,
|
||||
@ -229,7 +229,7 @@ class ESP32BLETracker : public Component,
|
||||
/// Called when a `ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT` event is received.
|
||||
void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m);
|
||||
|
||||
int app_id_;
|
||||
int app_id_{0};
|
||||
|
||||
/// Vector of addresses that have already been printed in print_bt_device_info
|
||||
std::vector<uint64_t> already_discovered_;
|
||||
@ -242,10 +242,10 @@ class ESP32BLETracker : public Component,
|
||||
uint32_t scan_duration_;
|
||||
uint32_t scan_interval_;
|
||||
uint32_t scan_window_;
|
||||
uint8_t scan_start_fail_count_;
|
||||
uint8_t scan_start_fail_count_{0};
|
||||
bool scan_continuous_;
|
||||
bool scan_active_;
|
||||
bool scanner_idle_;
|
||||
bool scanner_idle_{true};
|
||||
bool ble_was_disabled_{true};
|
||||
bool raw_advertisements_{false};
|
||||
bool parse_advertisements_{false};
|
||||
@ -260,6 +260,10 @@ class ESP32BLETracker : public Component,
|
||||
esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_;
|
||||
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
|
||||
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
|
||||
int connecting_{0};
|
||||
int discovered_{0};
|
||||
int searching_{0};
|
||||
int disconnecting_{0};
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE
|
||||
|
@ -112,7 +112,7 @@ void ESP32ImprovComponent::loop() {
|
||||
this->set_state_(improv::STATE_AUTHORIZED);
|
||||
} else
|
||||
#else
|
||||
this->set_state_(improv::STATE_AUTHORIZED);
|
||||
{ this->set_state_(improv::STATE_AUTHORIZED); }
|
||||
#endif
|
||||
{
|
||||
if (!this->check_identify_())
|
||||
|
@ -1,7 +1,8 @@
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
|
||||
from esphome.components import esp32
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
|
||||
@ -36,8 +37,32 @@ RMT_CHANNEL_ENUMS = {
|
||||
}
|
||||
|
||||
|
||||
def validate_rmt_channel(*, tx: bool):
|
||||
def use_new_rmt_driver():
|
||||
framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
if CORE.using_esp_idf and framework_version >= cv.Version(5, 0, 0):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def validate_clock_resolution():
|
||||
def _validator(value):
|
||||
cv.only_on_esp32(value)
|
||||
value = cv.int_(value)
|
||||
variant = esp32.get_esp32_variant()
|
||||
if variant == esp32.const.VARIANT_ESP32H2 and value > 32000000:
|
||||
raise cv.Invalid(
|
||||
f"ESP32 variant {variant} has a max clock_resolution of 32000000."
|
||||
)
|
||||
if value > 80000000:
|
||||
raise cv.Invalid(
|
||||
f"ESP32 variant {variant} has a max clock_resolution of 80000000."
|
||||
)
|
||||
return value
|
||||
|
||||
return _validator
|
||||
|
||||
|
||||
def validate_rmt_channel(*, tx: bool):
|
||||
rmt_channels = RMT_TX_CHANNELS if tx else RMT_RX_CHANNELS
|
||||
|
||||
def _validator(value):
|
||||
|
@ -1,5 +1,5 @@
|
||||
#include <cinttypes>
|
||||
#include "led_strip.h"
|
||||
#include <cinttypes>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@ -13,9 +13,13 @@ namespace esp32_rmt_led_strip {
|
||||
|
||||
static const char *const TAG = "esp32_rmt_led_strip";
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32H2
|
||||
static const uint32_t RMT_CLK_FREQ = 32000000;
|
||||
static const uint8_t RMT_CLK_DIV = 1;
|
||||
#else
|
||||
static const uint32_t RMT_CLK_FREQ = 80000000;
|
||||
|
||||
static const uint8_t RMT_CLK_DIV = 2;
|
||||
#endif
|
||||
|
||||
void ESP32RMTLEDStripLightOutput::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up ESP32 LED Strip...");
|
||||
@ -29,6 +33,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
memset(this->buf_, 0, buffer_size);
|
||||
|
||||
this->effect_data_ = allocator.allocate(this->num_leds_);
|
||||
if (this->effect_data_ == nullptr) {
|
||||
@ -37,9 +42,48 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
RAMAllocator<rmt_symbol_word_t> rmt_allocator(this->use_psram_ ? 0 : RAMAllocator<rmt_symbol_word_t>::ALLOC_INTERNAL);
|
||||
|
||||
// 8 bits per byte, 1 rmt_symbol_word_t per bit + 1 rmt_symbol_word_t for reset
|
||||
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1);
|
||||
|
||||
rmt_tx_channel_config_t channel;
|
||||
memset(&channel, 0, sizeof(channel));
|
||||
channel.clk_src = RMT_CLK_SRC_DEFAULT;
|
||||
channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV;
|
||||
channel.gpio_num = gpio_num_t(this->pin_);
|
||||
channel.mem_block_symbols = this->rmt_symbols_;
|
||||
channel.trans_queue_depth = 1;
|
||||
channel.flags.io_loop_back = 0;
|
||||
channel.flags.io_od_mode = 0;
|
||||
channel.flags.invert_out = 0;
|
||||
channel.flags.with_dma = 0;
|
||||
channel.intr_priority = 0;
|
||||
if (rmt_new_tx_channel(&channel, &this->channel_) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Channel creation failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
rmt_copy_encoder_config_t encoder;
|
||||
memset(&encoder, 0, sizeof(encoder));
|
||||
if (rmt_new_copy_encoder(&encoder, &this->encoder_) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Encoder creation failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (rmt_enable(this->channel_) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Enabling channel failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#else
|
||||
RAMAllocator<rmt_item32_t> rmt_allocator(this->use_psram_ ? 0 : RAMAllocator<rmt_item32_t>::ALLOC_INTERNAL);
|
||||
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 +
|
||||
1); // 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset
|
||||
|
||||
// 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset
|
||||
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1);
|
||||
|
||||
rmt_config_t config;
|
||||
memset(&config, 0, sizeof(config));
|
||||
@ -64,6 +108,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
|
||||
@ -100,7 +145,12 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
|
||||
|
||||
ESP_LOGVV(TAG, "Writing RGB values to bus...");
|
||||
|
||||
if (rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000)) != ESP_OK) {
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
esp_err_t error = rmt_tx_wait_all_done(this->channel_, 1000);
|
||||
#else
|
||||
esp_err_t error = rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000));
|
||||
#endif
|
||||
if (error != ESP_OK) {
|
||||
ESP_LOGE(TAG, "RMT TX timeout");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
@ -112,7 +162,11 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
|
||||
size_t size = 0;
|
||||
size_t len = 0;
|
||||
uint8_t *psrc = this->buf_;
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
rmt_symbol_word_t *pdest = this->rmt_buf_;
|
||||
#else
|
||||
rmt_item32_t *pdest = this->rmt_buf_;
|
||||
#endif
|
||||
while (size < buffer_size) {
|
||||
uint8_t b = *psrc;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
@ -130,7 +184,16 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
|
||||
len++;
|
||||
}
|
||||
|
||||
if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) {
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
rmt_transmit_config_t config;
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.loop_count = 0;
|
||||
config.flags.eot_level = 0;
|
||||
error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config);
|
||||
#else
|
||||
error = rmt_write_items(this->channel_, this->rmt_buf_, len, false);
|
||||
#endif
|
||||
if (error != ESP_OK) {
|
||||
ESP_LOGE(TAG, "RMT TX error");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
@ -186,7 +249,11 @@ light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index
|
||||
void ESP32RMTLEDStripLightOutput::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ESP32 RMT LED Strip:");
|
||||
ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_);
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
ESP_LOGCONFIG(TAG, " RMT Symbols: %" PRIu32, this->rmt_symbols_);
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_);
|
||||
#endif
|
||||
const char *rgb_order;
|
||||
switch (this->rgb_order_) {
|
||||
case ORDER_RGB:
|
||||
|
@ -9,8 +9,14 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/rmt.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_idf_version.h>
|
||||
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
#include <driver/rmt_tx.h>
|
||||
#else
|
||||
#include <driver/rmt.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_rmt_led_strip {
|
||||
@ -54,7 +60,11 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
|
||||
uint32_t reset_time_high, uint32_t reset_time_low);
|
||||
|
||||
void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; }
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; }
|
||||
#else
|
||||
void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; }
|
||||
#endif
|
||||
|
||||
void clear_effect_data() override {
|
||||
for (int i = 0; i < this->size(); i++)
|
||||
@ -70,7 +80,17 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
|
||||
|
||||
uint8_t *buf_{nullptr};
|
||||
uint8_t *effect_data_{nullptr};
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
rmt_channel_handle_t channel_{nullptr};
|
||||
rmt_encoder_handle_t encoder_{nullptr};
|
||||
rmt_symbol_word_t *rmt_buf_{nullptr};
|
||||
rmt_symbol_word_t bit0_, bit1_, reset_;
|
||||
uint32_t rmt_symbols_;
|
||||
#else
|
||||
rmt_item32_t *rmt_buf_{nullptr};
|
||||
rmt_item32_t bit0_, bit1_, reset_;
|
||||
rmt_channel_t channel_{RMT_CHANNEL_0};
|
||||
#endif
|
||||
|
||||
uint8_t pin_;
|
||||
uint16_t num_leds_;
|
||||
@ -78,9 +98,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
|
||||
bool is_wrgb_;
|
||||
bool use_psram_;
|
||||
|
||||
rmt_item32_t bit0_, bit1_, reset_;
|
||||
RGBOrder rgb_order_;
|
||||
rmt_channel_t channel_;
|
||||
|
||||
uint32_t last_refresh_{0};
|
||||
optional<uint32_t> max_refresh_rate_{};
|
||||
|
@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@ -13,7 +14,11 @@ from esphome.const import (
|
||||
CONF_PIN,
|
||||
CONF_RGB_ORDER,
|
||||
CONF_RMT_CHANNEL,
|
||||
CONF_RMT_SYMBOLS,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
@ -23,8 +28,6 @@ ESP32RMTLEDStripLightOutput = esp32_rmt_led_strip_ns.class_(
|
||||
"ESP32RMTLEDStripLightOutput", light.AddressableLight
|
||||
)
|
||||
|
||||
rmt_channel_t = cg.global_ns.enum("rmt_channel_t")
|
||||
|
||||
RGBOrder = esp32_rmt_led_strip_ns.enum("RGBOrder")
|
||||
|
||||
RGB_ORDERS = {
|
||||
@ -65,6 +68,53 @@ CONF_RESET_HIGH = "reset_high"
|
||||
CONF_RESET_LOW = "reset_low"
|
||||
|
||||
|
||||
class OptionalForIDF5(cv.SplitDefault):
|
||||
@property
|
||||
def default(self):
|
||||
if not esp32_rmt.use_new_rmt_driver():
|
||||
return cv.UNDEFINED
|
||||
return super().default
|
||||
|
||||
@default.setter
|
||||
def default(self, value):
|
||||
# Ignore default set from vol.Optional
|
||||
pass
|
||||
|
||||
|
||||
def only_with_new_rmt_driver(obj):
|
||||
if not esp32_rmt.use_new_rmt_driver():
|
||||
raise cv.Invalid(
|
||||
"This feature is only available for the IDF framework version 5."
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
def not_with_new_rmt_driver(obj):
|
||||
if esp32_rmt.use_new_rmt_driver():
|
||||
raise cv.Invalid(
|
||||
"This feature is not available for the IDF framework version 5."
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
def final_validation(config):
|
||||
if not esp32_rmt.use_new_rmt_driver():
|
||||
if CONF_RMT_CHANNEL not in config:
|
||||
if CORE.using_esp_idf:
|
||||
raise cv.Invalid(
|
||||
"rmt_channel is a required option for IDF version < 5."
|
||||
)
|
||||
raise cv.Invalid(
|
||||
"rmt_channel is a required option for the Arduino framework."
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"RMT_LED_STRIP support for IDF version < 5 is deprecated and will be removed soon."
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validation
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
|
||||
{
|
||||
@ -72,7 +122,18 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
|
||||
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
|
||||
cv.Required(CONF_RMT_CHANNEL): esp32_rmt.validate_rmt_channel(tx=True),
|
||||
cv.Optional(CONF_RMT_CHANNEL): cv.All(
|
||||
not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True)
|
||||
),
|
||||
OptionalForIDF5(
|
||||
CONF_RMT_SYMBOLS,
|
||||
esp32_idf=64,
|
||||
esp32_s2_idf=64,
|
||||
esp32_s3_idf=48,
|
||||
esp32_c3_idf=48,
|
||||
esp32_c6_idf=48,
|
||||
esp32_h2_idf=48,
|
||||
): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)),
|
||||
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
|
||||
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
|
||||
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
|
||||
@ -148,6 +209,10 @@ async def to_code(config):
|
||||
cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
|
||||
cg.add(var.set_use_psram(config[CONF_USE_PSRAM]))
|
||||
|
||||
if esp32_rmt.use_new_rmt_driver():
|
||||
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
|
||||
else:
|
||||
rmt_channel_t = cg.global_ns.enum("rmt_channel_t")
|
||||
cg.add(
|
||||
var.set_rmt_channel(
|
||||
getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}")
|
||||
|
@ -94,11 +94,11 @@ CLK_MODES = {
|
||||
|
||||
MANUAL_IP_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_STATIC_IP): cv.ipv4,
|
||||
cv.Required(CONF_GATEWAY): cv.ipv4,
|
||||
cv.Required(CONF_SUBNET): cv.ipv4,
|
||||
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4,
|
||||
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4,
|
||||
cv.Required(CONF_STATIC_IP): cv.ipv4address,
|
||||
cv.Required(CONF_GATEWAY): cv.ipv4address,
|
||||
cv.Required(CONF_SUBNET): cv.ipv4address,
|
||||
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4address,
|
||||
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4address,
|
||||
}
|
||||
)
|
||||
|
||||
@ -255,11 +255,11 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
def manual_ip(config):
|
||||
return cg.StructInitializer(
|
||||
ManualIP,
|
||||
("static_ip", IPAddress(*config[CONF_STATIC_IP].args)),
|
||||
("gateway", IPAddress(*config[CONF_GATEWAY].args)),
|
||||
("subnet", IPAddress(*config[CONF_SUBNET].args)),
|
||||
("dns1", IPAddress(*config[CONF_DNS1].args)),
|
||||
("dns2", IPAddress(*config[CONF_DNS2].args)),
|
||||
("static_ip", IPAddress(str(config[CONF_STATIC_IP]))),
|
||||
("gateway", IPAddress(str(config[CONF_GATEWAY]))),
|
||||
("subnet", IPAddress(str(config[CONF_SUBNET]))),
|
||||
("dns1", IPAddress(str(config[CONF_DNS1]))),
|
||||
("dns2", IPAddress(str(config[CONF_DNS2]))),
|
||||
)
|
||||
|
||||
|
||||
|
@ -8,11 +8,13 @@ namespace event {
|
||||
static const char *const TAG = "event";
|
||||
|
||||
void Event::trigger(const std::string &event_type) {
|
||||
if (types_.find(event_type) == types_.end()) {
|
||||
auto found = types_.find(event_type);
|
||||
if (found == types_.end()) {
|
||||
ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str());
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), event_type.c_str());
|
||||
last_event_type = &(*found);
|
||||
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str());
|
||||
this->event_callback_.call(event_type);
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,8 @@ namespace event {
|
||||
|
||||
class Event : public EntityBase, public EntityBase_DeviceClass {
|
||||
public:
|
||||
const std::string *last_event_type;
|
||||
|
||||
void trigger(const std::string &event_type);
|
||||
void set_event_types(const std::set<std::string> &event_types) { this->types_ = event_types; }
|
||||
std::set<std::string> get_event_types() const { return this->types_; }
|
||||
|
@ -51,8 +51,11 @@ CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs"
|
||||
# Cache loaded freetype fonts
|
||||
class FontCache(dict):
|
||||
def __missing__(self, key):
|
||||
try:
|
||||
res = self[key] = freetype.Face(key)
|
||||
return res
|
||||
except freetype.FT_Exception as e:
|
||||
raise cv.Invalid(f"Could not load Font file {key}: {e}") from e
|
||||
|
||||
|
||||
FONT_CACHE = FontCache()
|
||||
|
@ -133,9 +133,11 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
|
||||
auto diff_r = (float) color.r - (float) background.r;
|
||||
auto diff_g = (float) color.g - (float) background.g;
|
||||
auto diff_b = (float) color.b - (float) background.b;
|
||||
auto diff_w = (float) color.w - (float) background.w;
|
||||
auto b_r = (float) background.r;
|
||||
auto b_g = (float) background.g;
|
||||
auto b_b = (float) background.g;
|
||||
auto b_b = (float) background.b;
|
||||
auto b_w = (float) background.w;
|
||||
for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) {
|
||||
for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) {
|
||||
uint8_t pixel = 0;
|
||||
@ -153,8 +155,8 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
|
||||
display->draw_pixel_at(glyph_x, glyph_y, color);
|
||||
} else if (pixel != 0) {
|
||||
auto on = (float) pixel / (float) bpp_max;
|
||||
auto blended =
|
||||
Color((uint8_t) (diff_r * on + b_r), (uint8_t) (diff_g * on + b_g), (uint8_t) (diff_b * on + b_b));
|
||||
auto blended = Color((uint8_t) (diff_r * on + b_r), (uint8_t) (diff_g * on + b_g),
|
||||
(uint8_t) (diff_b * on + b_b), (uint8_t) (diff_w * on + b_w));
|
||||
display->draw_pixel_at(glyph_x, glyph_y, blended);
|
||||
}
|
||||
}
|
||||
|
@ -97,8 +97,9 @@ void GCJA5Component::parse_data_() {
|
||||
if (this->rx_message_[0] != 0x02 || this->rx_message_[31] != 0x03 || !this->calculate_checksum_()) {
|
||||
ESP_LOGVV(TAG, "Discarding bad packet - failed checks.");
|
||||
return;
|
||||
} else
|
||||
} else {
|
||||
ESP_LOGVV(TAG, "Good packet found.");
|
||||
}
|
||||
|
||||
this->have_good_data_ = true;
|
||||
uint8_t status = this->rx_message_[29];
|
||||
|
@ -342,9 +342,10 @@ bool HaierClimateBase::prepare_pending_action() {
|
||||
this->action_request_.reset();
|
||||
return false;
|
||||
}
|
||||
} else
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ClimateTraits HaierClimateBase::traits() { return traits_; }
|
||||
|
||||
|
@ -710,9 +710,10 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo
|
||||
alarm_code++;
|
||||
}
|
||||
active_alarms_[i] = packet[2 + i];
|
||||
} else
|
||||
} else {
|
||||
alarm_code += 8;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
float alarm_count = 0.0f;
|
||||
static uint8_t nibble_bits_count[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
|
||||
|
@ -87,9 +87,10 @@ void HeatpumpIRClimate::setup() {
|
||||
this->publish_state();
|
||||
});
|
||||
this->current_temperature = this->sensor_->state;
|
||||
} else
|
||||
} else {
|
||||
this->current_temperature = NAN;
|
||||
}
|
||||
}
|
||||
|
||||
void HeatpumpIRClimate::transmit_state() {
|
||||
uint8_t power_mode_cmd;
|
||||
|
@ -12,6 +12,8 @@
|
||||
#include "esp_crt_bundle.h"
|
||||
#endif
|
||||
|
||||
#include "esp_task_wdt.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace http_request {
|
||||
|
||||
@ -117,11 +119,11 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
App.feed_wdt();
|
||||
container->feed_wdt();
|
||||
container->content_length = esp_http_client_fetch_headers(client);
|
||||
App.feed_wdt();
|
||||
container->feed_wdt();
|
||||
container->status_code = esp_http_client_get_status_code(client);
|
||||
App.feed_wdt();
|
||||
container->feed_wdt();
|
||||
if (is_success(container->status_code)) {
|
||||
container->duration_ms = millis() - start;
|
||||
return container;
|
||||
@ -151,11 +153,11 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
App.feed_wdt();
|
||||
container->feed_wdt();
|
||||
container->content_length = esp_http_client_fetch_headers(client);
|
||||
App.feed_wdt();
|
||||
container->feed_wdt();
|
||||
container->status_code = esp_http_client_get_status_code(client);
|
||||
App.feed_wdt();
|
||||
container->feed_wdt();
|
||||
if (is_success(container->status_code)) {
|
||||
container->duration_ms = millis() - start;
|
||||
return container;
|
||||
@ -185,8 +187,9 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
App.feed_wdt();
|
||||
this->feed_wdt();
|
||||
int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
|
||||
this->feed_wdt();
|
||||
this->bytes_read_ += read_len;
|
||||
|
||||
this->duration_ms += (millis() - start);
|
||||
@ -201,6 +204,13 @@ void HttpContainerIDF::end() {
|
||||
esp_http_client_cleanup(this->client_);
|
||||
}
|
||||
|
||||
void HttpContainerIDF::feed_wdt() {
|
||||
// Tests to see if the executing task has a watchdog timer attached
|
||||
if (esp_task_wdt_status(nullptr) == ESP_OK) {
|
||||
App.feed_wdt();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace http_request
|
||||
} // namespace esphome
|
||||
|
||||
|
@ -18,6 +18,9 @@ class HttpContainerIDF : public HttpContainer {
|
||||
int read(uint8_t *buf, size_t max_len) override;
|
||||
void end() override;
|
||||
|
||||
/// @brief Feeds the watchdog timer if the executing task has one attached
|
||||
void feed_wdt();
|
||||
|
||||
protected:
|
||||
esp_http_client_handle_t client_;
|
||||
};
|
||||
|
@ -9,6 +9,13 @@
|
||||
namespace esphome {
|
||||
namespace http_request {
|
||||
|
||||
// The update function runs in a task only on ESP32s.
|
||||
#ifdef USE_ESP32
|
||||
#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task
|
||||
#else
|
||||
#define UPDATE_RETURN return
|
||||
#endif
|
||||
|
||||
static const char *const TAG = "http_request.update";
|
||||
|
||||
static const size_t MAX_READ_SIZE = 256;
|
||||
@ -29,45 +36,57 @@ void HttpRequestUpdate::setup() {
|
||||
}
|
||||
|
||||
void HttpRequestUpdate::update() {
|
||||
auto container = this->request_parent_->get(this->source_url_);
|
||||
#ifdef USE_ESP32
|
||||
xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
|
||||
#else
|
||||
this->update_task(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
void HttpRequestUpdate::update_task(void *params) {
|
||||
HttpRequestUpdate *this_update = (HttpRequestUpdate *) params;
|
||||
|
||||
auto container = this_update->request_parent_->get(this_update->source_url_);
|
||||
|
||||
if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
|
||||
std::string msg = str_sprintf("Failed to fetch manifest from %s", this->source_url_.c_str());
|
||||
this->status_set_error(msg.c_str());
|
||||
return;
|
||||
std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str());
|
||||
this_update->status_set_error(msg.c_str());
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
uint8_t *data = allocator.allocate(container->content_length);
|
||||
if (data == nullptr) {
|
||||
std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length);
|
||||
this->status_set_error(msg.c_str());
|
||||
this_update->status_set_error(msg.c_str());
|
||||
container->end();
|
||||
return;
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
size_t read_index = 0;
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
|
||||
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
read_index += read_bytes;
|
||||
}
|
||||
|
||||
bool valid = false;
|
||||
{ // Ensures the response string falls out of scope and deallocates before the task ends
|
||||
std::string response((char *) data, read_index);
|
||||
allocator.deallocate(data, container->content_length);
|
||||
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
bool valid = json::parse_json(response, [this](JsonObject root) -> bool {
|
||||
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
|
||||
if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
this->update_info_.title = root["name"].as<std::string>();
|
||||
this->update_info_.latest_version = root["version"].as<std::string>();
|
||||
this_update->update_info_.title = root["name"].as<std::string>();
|
||||
this_update->update_info_.latest_version = root["version"].as<std::string>();
|
||||
|
||||
for (auto build : root["builds"].as<JsonArray>()) {
|
||||
if (!build.containsKey("chipFamily")) {
|
||||
@ -84,38 +103,40 @@ void HttpRequestUpdate::update() {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
this->update_info_.firmware_url = ota["path"].as<std::string>();
|
||||
this->update_info_.md5 = ota["md5"].as<std::string>();
|
||||
this_update->update_info_.firmware_url = ota["path"].as<std::string>();
|
||||
this_update->update_info_.md5 = ota["md5"].as<std::string>();
|
||||
|
||||
if (ota.containsKey("summary"))
|
||||
this->update_info_.summary = ota["summary"].as<std::string>();
|
||||
this_update->update_info_.summary = ota["summary"].as<std::string>();
|
||||
if (ota.containsKey("release_url"))
|
||||
this->update_info_.release_url = ota["release_url"].as<std::string>();
|
||||
this_update->update_info_.release_url = ota["release_url"].as<std::string>();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
std::string msg = str_sprintf("Failed to parse JSON from %s", this->source_url_.c_str());
|
||||
this->status_set_error(msg.c_str());
|
||||
return;
|
||||
std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str());
|
||||
this_update->status_set_error(msg.c_str());
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
// Merge source_url_ and this->update_info_.firmware_url
|
||||
if (this->update_info_.firmware_url.find("http") == std::string::npos) {
|
||||
std::string path = this->update_info_.firmware_url;
|
||||
// Merge source_url_ and this_update->update_info_.firmware_url
|
||||
if (this_update->update_info_.firmware_url.find("http") == std::string::npos) {
|
||||
std::string path = this_update->update_info_.firmware_url;
|
||||
if (path[0] == '/') {
|
||||
std::string domain = this->source_url_.substr(0, this->source_url_.find('/', 8));
|
||||
this->update_info_.firmware_url = domain + path;
|
||||
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
|
||||
this_update->update_info_.firmware_url = domain + path;
|
||||
} else {
|
||||
std::string domain = this->source_url_.substr(0, this->source_url_.rfind('/') + 1);
|
||||
this->update_info_.firmware_url = domain + path;
|
||||
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
|
||||
this_update->update_info_.firmware_url = domain + path;
|
||||
}
|
||||
}
|
||||
|
||||
{ // Ensures the current version string falls out of scope and deallocates before the task ends
|
||||
std::string current_version;
|
||||
#ifdef ESPHOME_PROJECT_VERSION
|
||||
current_version = ESPHOME_PROJECT_VERSION;
|
||||
@ -123,19 +144,23 @@ void HttpRequestUpdate::update() {
|
||||
current_version = ESPHOME_VERSION;
|
||||
#endif
|
||||
|
||||
this->update_info_.current_version = current_version;
|
||||
|
||||
if (this->update_info_.latest_version.empty() || this->update_info_.latest_version == update_info_.current_version) {
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
} else {
|
||||
this->state_ = update::UPDATE_STATE_AVAILABLE;
|
||||
this_update->update_info_.current_version = current_version;
|
||||
}
|
||||
|
||||
this->update_info_.has_progress = false;
|
||||
this->update_info_.progress = 0.0f;
|
||||
if (this_update->update_info_.latest_version.empty() ||
|
||||
this_update->update_info_.latest_version == this_update->update_info_.current_version) {
|
||||
this_update->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
} else {
|
||||
this_update->state_ = update::UPDATE_STATE_AVAILABLE;
|
||||
}
|
||||
|
||||
this->status_clear_error();
|
||||
this->publish_state();
|
||||
this_update->update_info_.has_progress = false;
|
||||
this_update->update_info_.progress = 0.0f;
|
||||
|
||||
this_update->status_clear_error();
|
||||
this_update->publish_state();
|
||||
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
void HttpRequestUpdate::perform(bool force) {
|
||||
|
@ -7,6 +7,10 @@
|
||||
#include "esphome/components/http_request/ota/ota_http_request.h"
|
||||
#include "esphome/components/update/update_entity.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace http_request {
|
||||
|
||||
@ -29,6 +33,11 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent {
|
||||
HttpRequestComponent *request_parent_;
|
||||
OtaHttpRequestComponent *ota_parent_;
|
||||
std::string source_url_;
|
||||
|
||||
static void update_task(void *params);
|
||||
#ifdef USE_ESP32
|
||||
TaskHandle_t update_task_handle_{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace http_request
|
||||
|
@ -17,14 +17,14 @@ void IDFI2CBus::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up I2C bus...");
|
||||
static i2c_port_t next_port = I2C_NUM_0;
|
||||
port_ = next_port;
|
||||
#if I2C_NUM_MAX > 1
|
||||
#if SOC_I2C_NUM > 1
|
||||
next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX;
|
||||
#else
|
||||
next_port = I2C_NUM_MAX;
|
||||
#endif
|
||||
|
||||
if (port_ == I2C_NUM_MAX) {
|
||||
ESP_LOGE(TAG, "Too many I2C buses configured");
|
||||
ESP_LOGE(TAG, "Too many I2C buses configured. Max %u supported.", SOC_I2C_NUM);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ void I2SAudioMicrophone::setup() {
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (this->pdm_) {
|
||||
if (this->parent_->get_port() != I2S_NUM_0) {
|
||||
ESP_LOGE(TAG, "PDM only works on I2S0!");
|
||||
@ -33,6 +34,7 @@ void I2SAudioMicrophone::setup() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void I2SAudioMicrophone::start() {
|
||||
if (this->is_failed())
|
||||
|
@ -247,7 +247,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
|
||||
|
||||
// Ensure ring buffer is at least as large as the total size of the DMA buffers
|
||||
const size_t ring_buffer_size =
|
||||
std::min((uint32_t) dma_buffers_size, this_speaker->buffer_duration_ms_ * bytes_per_ms);
|
||||
std::max((uint32_t) dma_buffers_size, this_speaker->buffer_duration_ms_ * bytes_per_ms);
|
||||
|
||||
if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(dma_buffers_size, ring_buffer_size))) {
|
||||
// Failed to allocate buffers
|
||||
|
@ -1,9 +1,12 @@
|
||||
import logging
|
||||
|
||||
from esphome import core, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
from esphome.components.display import validate_rotation
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_COLOR_ORDER,
|
||||
CONF_COLOR_PALETTE,
|
||||
CONF_DC_PIN,
|
||||
@ -27,17 +30,12 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
|
||||
def AUTO_LOAD():
|
||||
if CORE.is_esp32:
|
||||
return ["psram"]
|
||||
return []
|
||||
|
||||
|
||||
CODEOWNERS = ["@nielsnl68", "@clydebarrow"]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx")
|
||||
ILI9XXXDisplay = ili9xxx_ns.class_(
|
||||
@ -84,7 +82,7 @@ COLOR_ORDERS = {
|
||||
"BGR": ColorOrder.COLOR_ORDER_BGR,
|
||||
}
|
||||
|
||||
COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE")
|
||||
COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE", "8BIT", upper=True)
|
||||
|
||||
CONF_LED_PIN = "led_pin"
|
||||
CONF_COLOR_PALETTE_IMAGES = "color_palette_images"
|
||||
@ -195,11 +193,29 @@ CONFIG_SCHEMA = cv.All(
|
||||
_validate,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
|
||||
def final_validate(config):
|
||||
global_config = full_config.get()
|
||||
# Ideally would calculate buffer size here, but that info is not available on the Python side
|
||||
needs_buffer = (
|
||||
CONF_LAMBDA in config or CONF_PAGES in config or config[CONF_AUTO_CLEAR_ENABLED]
|
||||
)
|
||||
if (
|
||||
CORE.is_esp32
|
||||
and config[CONF_COLOR_PALETTE] == "NONE"
|
||||
and "psram" not in global_config
|
||||
and needs_buffer
|
||||
):
|
||||
LOGGER.info("Consider enabling PSRAM if available for the display buffer")
|
||||
|
||||
return spi.final_validate_device_schema(
|
||||
"ili9xxx", require_miso=False, require_mosi=True
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
rhs = MODELS[config[CONF_MODEL]].new()
|
||||
var = cg.Pvariable(config[CONF_ID], rhs)
|
||||
@ -283,6 +299,8 @@ async def to_code(config):
|
||||
palette = converted.getpalette()
|
||||
assert len(palette) == 256 * 3
|
||||
rhs = palette
|
||||
elif config[CONF_COLOR_PALETTE] == "8BIT":
|
||||
cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8))
|
||||
else:
|
||||
cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_16))
|
||||
|
||||
|
@ -66,12 +66,9 @@ void ILI9XXXDisplay::setup() {
|
||||
void ILI9XXXDisplay::alloc_buffer_() {
|
||||
if (this->buffer_color_mode_ == BITS_16) {
|
||||
this->init_internal_(this->get_buffer_length_() * 2);
|
||||
if (this->buffer_ != nullptr) {
|
||||
return;
|
||||
}
|
||||
this->buffer_color_mode_ = BITS_8;
|
||||
}
|
||||
} else {
|
||||
this->init_internal_(this->get_buffer_length_());
|
||||
}
|
||||
if (this->buffer_ == nullptr) {
|
||||
this->mark_failed();
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
|
||||
protected:
|
||||
inline bool check_buffer_() {
|
||||
if (this->buffer_ == nullptr) {
|
||||
if (!this->is_failed())
|
||||
this->alloc_buffer_();
|
||||
return !this->is_failed();
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import puremagic
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from esphome import core, external_files
|
||||
import esphome.codegen as cg
|
||||
@ -29,21 +29,259 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "image"
|
||||
DEPENDENCIES = ["display"]
|
||||
MULTI_CONF = True
|
||||
MULTI_CONF_NO_DEFAULT = True
|
||||
|
||||
image_ns = cg.esphome_ns.namespace("image")
|
||||
|
||||
ImageType = image_ns.enum("ImageType")
|
||||
|
||||
CONF_OPAQUE = "opaque"
|
||||
CONF_CHROMA_KEY = "chroma_key"
|
||||
CONF_ALPHA_CHANNEL = "alpha_channel"
|
||||
CONF_INVERT_ALPHA = "invert_alpha"
|
||||
|
||||
TRANSPARENCY_TYPES = (
|
||||
CONF_OPAQUE,
|
||||
CONF_CHROMA_KEY,
|
||||
CONF_ALPHA_CHANNEL,
|
||||
)
|
||||
|
||||
|
||||
def get_image_type_enum(type):
|
||||
return getattr(ImageType, f"IMAGE_TYPE_{type.upper()}")
|
||||
|
||||
|
||||
def get_transparency_enum(transparency):
|
||||
return getattr(TransparencyType, f"TRANSPARENCY_{transparency.upper()}")
|
||||
|
||||
|
||||
class ImageEncoder:
|
||||
"""
|
||||
Superclass of image type encoders
|
||||
"""
|
||||
|
||||
# Control which transparency options are available for a given type
|
||||
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE}
|
||||
|
||||
# All imageencoder types are valid
|
||||
@staticmethod
|
||||
def validate(value):
|
||||
return value
|
||||
|
||||
def __init__(self, width, height, transparency, dither, invert_alpha):
|
||||
"""
|
||||
:param width: The image width in pixels
|
||||
:param height: The image height in pixels
|
||||
:param transparency: Transparency type
|
||||
:param dither: Dither method
|
||||
:param invert_alpha: True if the alpha channel should be inverted; for monochrome formats inverts the colours.
|
||||
"""
|
||||
self.transparency = transparency
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.data = [0 for _ in range(width * height)]
|
||||
self.dither = dither
|
||||
self.index = 0
|
||||
self.invert_alpha = invert_alpha
|
||||
self.path = ""
|
||||
|
||||
def convert(self, image, path):
|
||||
"""
|
||||
Convert the image format
|
||||
:param image: Input image
|
||||
:param path: Path to the image file
|
||||
:return: converted image
|
||||
"""
|
||||
return image
|
||||
|
||||
def encode(self, pixel):
|
||||
"""
|
||||
Encode a single pixel
|
||||
"""
|
||||
|
||||
def end_row(self):
|
||||
"""
|
||||
Marks the end of a pixel row
|
||||
:return:
|
||||
"""
|
||||
|
||||
|
||||
def is_alpha_only(image: Image):
|
||||
"""
|
||||
Check if an image (assumed to be RGBA) is only alpha
|
||||
"""
|
||||
# Any alpha data?
|
||||
if image.split()[-1].getextrema()[0] == 0xFF:
|
||||
return False
|
||||
return all(b.getextrema()[1] == 0 for b in image.split()[:-1])
|
||||
|
||||
|
||||
class ImageBinary(ImageEncoder):
|
||||
allow_config = {CONF_OPAQUE, CONF_INVERT_ALPHA, CONF_CHROMA_KEY}
|
||||
|
||||
def __init__(self, width, height, transparency, dither, invert_alpha):
|
||||
self.width8 = (width + 7) // 8
|
||||
super().__init__(self.width8, height, transparency, dither, invert_alpha)
|
||||
self.bitno = 0
|
||||
|
||||
def convert(self, image, path):
|
||||
if is_alpha_only(image):
|
||||
image = image.split()[-1]
|
||||
return image.convert("1", dither=self.dither)
|
||||
|
||||
def encode(self, pixel):
|
||||
if self.invert_alpha:
|
||||
pixel = not pixel
|
||||
if pixel:
|
||||
self.data[self.index] |= 0x80 >> (self.bitno % 8)
|
||||
self.bitno += 1
|
||||
if self.bitno == 8:
|
||||
self.bitno = 0
|
||||
self.index += 1
|
||||
|
||||
def end_row(self):
|
||||
"""
|
||||
Pad rows to a byte boundary
|
||||
"""
|
||||
if self.bitno != 0:
|
||||
self.bitno = 0
|
||||
self.index += 1
|
||||
|
||||
|
||||
class ImageGrayscale(ImageEncoder):
|
||||
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_INVERT_ALPHA, CONF_OPAQUE}
|
||||
|
||||
def convert(self, image, path):
|
||||
if is_alpha_only(image):
|
||||
if self.transparency != CONF_ALPHA_CHANNEL:
|
||||
_LOGGER.warning(
|
||||
"Grayscale image %s is alpha only, but transparency is set to %s",
|
||||
path,
|
||||
self.transparency,
|
||||
)
|
||||
self.transparency = CONF_ALPHA_CHANNEL
|
||||
image = image.split()[-1]
|
||||
return image.convert("LA")
|
||||
|
||||
def encode(self, pixel):
|
||||
b, a = pixel
|
||||
if self.transparency == CONF_CHROMA_KEY:
|
||||
if b == 1:
|
||||
b = 0
|
||||
if a != 0xFF:
|
||||
b = 1
|
||||
if self.invert_alpha:
|
||||
b ^= 0xFF
|
||||
if self.transparency == CONF_ALPHA_CHANNEL:
|
||||
if a != 0xFF:
|
||||
b = a
|
||||
self.data[self.index] = b
|
||||
self.index += 1
|
||||
|
||||
|
||||
class ImageRGB565(ImageEncoder):
|
||||
def __init__(self, width, height, transparency, dither, invert_alpha):
|
||||
stride = 3 if transparency == CONF_ALPHA_CHANNEL else 2
|
||||
super().__init__(
|
||||
width * stride,
|
||||
height,
|
||||
transparency,
|
||||
dither,
|
||||
invert_alpha,
|
||||
)
|
||||
|
||||
def convert(self, image, path):
|
||||
return image.convert("RGBA")
|
||||
|
||||
def encode(self, pixel):
|
||||
r, g, b, a = pixel
|
||||
r = r >> 3
|
||||
g = g >> 2
|
||||
b = b >> 3
|
||||
if self.transparency == CONF_CHROMA_KEY:
|
||||
if r == 0 and g == 1 and b == 0:
|
||||
g = 0
|
||||
elif a < 128:
|
||||
r = 0
|
||||
g = 1
|
||||
b = 0
|
||||
rgb = (r << 11) | (g << 5) | b
|
||||
self.data[self.index] = rgb >> 8
|
||||
self.index += 1
|
||||
self.data[self.index] = rgb & 0xFF
|
||||
self.index += 1
|
||||
if self.transparency == CONF_ALPHA_CHANNEL:
|
||||
if self.invert_alpha:
|
||||
a ^= 0xFF
|
||||
self.data[self.index] = a
|
||||
self.index += 1
|
||||
|
||||
|
||||
class ImageRGB(ImageEncoder):
|
||||
def __init__(self, width, height, transparency, dither, invert_alpha):
|
||||
stride = 4 if transparency == CONF_ALPHA_CHANNEL else 3
|
||||
super().__init__(
|
||||
width * stride,
|
||||
height,
|
||||
transparency,
|
||||
dither,
|
||||
invert_alpha,
|
||||
)
|
||||
|
||||
def convert(self, image, path):
|
||||
return image.convert("RGBA")
|
||||
|
||||
def encode(self, pixel):
|
||||
r, g, b, a = pixel
|
||||
if self.transparency == CONF_CHROMA_KEY:
|
||||
if r == 0 and g == 1 and b == 0:
|
||||
g = 0
|
||||
elif a < 128:
|
||||
r = 0
|
||||
g = 1
|
||||
b = 0
|
||||
self.data[self.index] = r
|
||||
self.index += 1
|
||||
self.data[self.index] = g
|
||||
self.index += 1
|
||||
self.data[self.index] = b
|
||||
self.index += 1
|
||||
if self.transparency == CONF_ALPHA_CHANNEL:
|
||||
if self.invert_alpha:
|
||||
a ^= 0xFF
|
||||
self.data[self.index] = a
|
||||
self.index += 1
|
||||
|
||||
|
||||
class ReplaceWith:
|
||||
"""
|
||||
Placeholder class to provide feedback on deprecated features
|
||||
"""
|
||||
|
||||
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE}
|
||||
|
||||
def __init__(self, replace_with):
|
||||
self.replace_with = replace_with
|
||||
|
||||
def validate(self, value):
|
||||
raise cv.Invalid(
|
||||
f"Image type {value} is removed; replace with {self.replace_with}"
|
||||
)
|
||||
|
||||
|
||||
IMAGE_TYPE = {
|
||||
"BINARY": ImageType.IMAGE_TYPE_BINARY,
|
||||
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY,
|
||||
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
|
||||
"RGB565": ImageType.IMAGE_TYPE_RGB565,
|
||||
"RGB24": ImageType.IMAGE_TYPE_RGB24,
|
||||
"RGBA": ImageType.IMAGE_TYPE_RGBA,
|
||||
"BINARY": ImageBinary,
|
||||
"GRAYSCALE": ImageGrayscale,
|
||||
"RGB565": ImageRGB565,
|
||||
"RGB": ImageRGB,
|
||||
"TRANSPARENT_BINARY": ReplaceWith(
|
||||
"'type: BINARY' and 'use_transparency: chroma_key'"
|
||||
),
|
||||
"RGB24": ReplaceWith("'type: RGB'"),
|
||||
"RGBA": ReplaceWith("'type: RGB' and 'use_transparency: alpha_channel'"),
|
||||
}
|
||||
|
||||
TransparencyType = image_ns.enum("TransparencyType")
|
||||
|
||||
CONF_USE_TRANSPARENCY = "use_transparency"
|
||||
|
||||
# If the MDI file cannot be downloaded within this time, abort.
|
||||
@ -53,17 +291,11 @@ SOURCE_LOCAL = "local"
|
||||
SOURCE_MDI = "mdi"
|
||||
SOURCE_WEB = "web"
|
||||
|
||||
|
||||
Image_ = image_ns.class_("Image")
|
||||
|
||||
|
||||
def _compute_local_icon_path(value: dict) -> Path:
|
||||
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi"
|
||||
return base_dir / f"{value[CONF_ICON]}.svg"
|
||||
|
||||
|
||||
def compute_local_image_path(value: dict) -> Path:
|
||||
url = value[CONF_URL]
|
||||
def compute_local_image_path(value) -> Path:
|
||||
url = value[CONF_URL] if isinstance(value, dict) else value
|
||||
h = hashlib.new("sha256")
|
||||
h.update(url.encode())
|
||||
key = h.hexdigest()[:8]
|
||||
@ -71,30 +303,38 @@ def compute_local_image_path(value: dict) -> Path:
|
||||
return base_dir / key
|
||||
|
||||
|
||||
def download_mdi(value):
|
||||
validate_cairosvg_installed(value)
|
||||
def local_path(value):
|
||||
value = value[CONF_PATH] if isinstance(value, dict) else value
|
||||
return str(CORE.relative_config_path(value))
|
||||
|
||||
mdi_id = value[CONF_ICON]
|
||||
path = _compute_local_icon_path(value)
|
||||
|
||||
def download_file(url, path):
|
||||
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
|
||||
return str(path)
|
||||
|
||||
|
||||
def download_mdi(value):
|
||||
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
|
||||
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi"
|
||||
path = base_dir / f"{mdi_id}.svg"
|
||||
|
||||
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
|
||||
|
||||
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
|
||||
|
||||
return value
|
||||
return download_file(url, path)
|
||||
|
||||
|
||||
def download_image(value):
|
||||
url = value[CONF_URL]
|
||||
path = compute_local_image_path(value)
|
||||
|
||||
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
|
||||
|
||||
return value
|
||||
value = value[CONF_URL] if isinstance(value, dict) else value
|
||||
return download_file(value, compute_local_image_path(value))
|
||||
|
||||
|
||||
def validate_cairosvg_installed(value):
|
||||
"""Validate that cairosvg is installed"""
|
||||
def is_svg_file(file):
|
||||
if not file:
|
||||
return False
|
||||
with open(file, "rb") as f:
|
||||
return "<svg" in str(f.read(1024))
|
||||
|
||||
|
||||
def validate_cairosvg_installed():
|
||||
try:
|
||||
import cairosvg
|
||||
except ImportError as err:
|
||||
@ -110,73 +350,28 @@ def validate_cairosvg_installed(value):
|
||||
"(pip install -U cairosvg)"
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def validate_cross_dependencies(config):
|
||||
"""
|
||||
Validate fields whose possible values depend on other fields.
|
||||
For example, validate that explicitly transparent image types
|
||||
have "use_transparency" set to True.
|
||||
Also set the default value for those kind of dependent fields.
|
||||
"""
|
||||
is_mdi = CONF_FILE in config and config[CONF_FILE][CONF_SOURCE] == SOURCE_MDI
|
||||
if CONF_TYPE not in config:
|
||||
if is_mdi:
|
||||
config[CONF_TYPE] = "TRANSPARENT_BINARY"
|
||||
else:
|
||||
config[CONF_TYPE] = "BINARY"
|
||||
|
||||
image_type = config[CONF_TYPE]
|
||||
is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"]
|
||||
|
||||
# If the use_transparency option was not specified, set the default depending on the image type
|
||||
if CONF_USE_TRANSPARENCY not in config:
|
||||
config[CONF_USE_TRANSPARENCY] = is_transparent_type
|
||||
|
||||
if is_transparent_type and not config[CONF_USE_TRANSPARENCY]:
|
||||
raise cv.Invalid(f"Image type {image_type} must always be transparent.")
|
||||
|
||||
if is_mdi and config[CONF_TYPE] not in ["BINARY", "TRANSPARENT_BINARY"]:
|
||||
raise cv.Invalid("MDI images must be binary images.")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def validate_file_shorthand(value):
|
||||
value = cv.string_strict(value)
|
||||
if value.startswith("mdi:"):
|
||||
validate_cairosvg_installed(value)
|
||||
|
||||
match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value)
|
||||
if match is None:
|
||||
raise cv.Invalid("Could not parse mdi icon name.")
|
||||
icon = match.group(1)
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_MDI,
|
||||
CONF_ICON: icon,
|
||||
}
|
||||
)
|
||||
return download_mdi(icon)
|
||||
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_WEB,
|
||||
CONF_URL: value,
|
||||
}
|
||||
)
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_LOCAL,
|
||||
CONF_PATH: value,
|
||||
}
|
||||
)
|
||||
return download_image(value)
|
||||
|
||||
value = cv.file_(value)
|
||||
return local_path(value)
|
||||
|
||||
|
||||
LOCAL_SCHEMA = cv.Schema(
|
||||
LOCAL_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_PATH): cv.file_,
|
||||
}
|
||||
},
|
||||
local_path,
|
||||
)
|
||||
|
||||
MDI_SCHEMA = cv.All(
|
||||
@ -203,205 +398,202 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||
)
|
||||
|
||||
|
||||
def _file_schema(value):
|
||||
if isinstance(value, str):
|
||||
return validate_file_shorthand(value)
|
||||
return TYPED_FILE_SCHEMA(value)
|
||||
def validate_transparency(choices=TRANSPARENCY_TYPES):
|
||||
def validate(value):
|
||||
if isinstance(value, bool):
|
||||
value = str(value)
|
||||
return cv.one_of(*choices, lower=True)(value)
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
FILE_SCHEMA = cv.Schema(_file_schema)
|
||||
def validate_type(image_types):
|
||||
def validate(value):
|
||||
value = cv.one_of(*image_types, upper=True)(value)
|
||||
return IMAGE_TYPE[value].validate(value)
|
||||
|
||||
IMAGE_SCHEMA = cv.Schema(
|
||||
cv.All(
|
||||
return validate
|
||||
|
||||
|
||||
def validate_settings(value):
|
||||
type = value[CONF_TYPE]
|
||||
transparency = value[CONF_USE_TRANSPARENCY].lower()
|
||||
allow_config = IMAGE_TYPE[type].allow_config
|
||||
if transparency not in allow_config:
|
||||
raise cv.Invalid(
|
||||
f"Image format '{type}' cannot have transparency: {transparency}"
|
||||
)
|
||||
invert_alpha = value.get(CONF_INVERT_ALPHA, False)
|
||||
if (
|
||||
invert_alpha
|
||||
and transparency != CONF_ALPHA_CHANNEL
|
||||
and CONF_INVERT_ALPHA not in allow_config
|
||||
):
|
||||
raise cv.Invalid("No alpha channel to invert")
|
||||
if file := value.get(CONF_FILE):
|
||||
file = Path(file)
|
||||
if is_svg_file(file):
|
||||
validate_cairosvg_installed()
|
||||
else:
|
||||
try:
|
||||
Image.open(file)
|
||||
except UnidentifiedImageError as exc:
|
||||
raise cv.Invalid(f"File can't be opened as image: {file}") from exc
|
||||
return value
|
||||
|
||||
|
||||
BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Image_),
|
||||
cv.Required(CONF_FILE): FILE_SCHEMA,
|
||||
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
|
||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||
# Not setting default here on purpose; the default depends on the source type
|
||||
# (file or mdi), and will be set in the "validate_cross_dependencies" validator.
|
||||
cv.Optional(CONF_TYPE): cv.enum(IMAGE_TYPE, upper=True),
|
||||
# Not setting default here on purpose; the default depends on the image type,
|
||||
# and thus will be set in the "validate_cross_dependencies" validator.
|
||||
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
|
||||
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
||||
"NONE", "FLOYDSTEINBERG", upper=True
|
||||
),
|
||||
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
},
|
||||
validate_cross_dependencies,
|
||||
)
|
||||
}
|
||||
).add_extra(validate_settings)
|
||||
|
||||
IMAGE_SCHEMA = BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||
cv.Optional(
|
||||
CONF_USE_TRANSPARENCY, default=CONF_OPAQUE
|
||||
): validate_transparency(),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = IMAGE_SCHEMA
|
||||
|
||||
|
||||
def load_svg_image(file: bytes, resize: tuple[int, int]):
|
||||
# Local imports only to allow "validate_pillow_installed" to run *before* importing it
|
||||
# cairosvg is only needed in case of SVG images; adding it
|
||||
# to the top would force configurations not using SVG to also have it
|
||||
# installed for no reason.
|
||||
from cairosvg import svg2png
|
||||
from PIL import Image
|
||||
|
||||
if resize:
|
||||
req_width, req_height = resize
|
||||
svg_image = svg2png(
|
||||
file,
|
||||
output_width=req_width,
|
||||
output_height=req_height,
|
||||
def typed_image_schema(image_type):
|
||||
"""
|
||||
Construct a schema for a specific image type, allowing transparency options
|
||||
"""
|
||||
return cv.Any(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(t.lower()): cv.ensure_list(
|
||||
BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_USE_TRANSPARENCY, default=t
|
||||
): validate_transparency((t,)),
|
||||
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
||||
(image_type,)
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
for t in IMAGE_TYPE[image_type].allow_config.intersection(
|
||||
TRANSPARENCY_TYPES
|
||||
)
|
||||
}
|
||||
),
|
||||
# Allow a default configuration with no transparency preselected
|
||||
cv.ensure_list(
|
||||
BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_USE_TRANSPARENCY, default=CONF_OPAQUE
|
||||
): validate_transparency(),
|
||||
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
||||
(image_type,)
|
||||
),
|
||||
}
|
||||
)
|
||||
),
|
||||
)
|
||||
else:
|
||||
svg_image = svg2png(file)
|
||||
|
||||
return Image.open(io.BytesIO(svg_image))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Local import only to allow "validate_pillow_installed" to run *before* importing it
|
||||
from PIL import Image
|
||||
# The config schema can be a (possibly empty) single list of images,
|
||||
# or a dictionary of image types each with a list of images
|
||||
CONFIG_SCHEMA = cv.Any(
|
||||
cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
|
||||
cv.ensure_list(IMAGE_SCHEMA),
|
||||
)
|
||||
|
||||
conf_file = config[CONF_FILE]
|
||||
|
||||
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
|
||||
path = CORE.relative_config_path(conf_file[CONF_PATH])
|
||||
|
||||
elif conf_file[CONF_SOURCE] == SOURCE_MDI:
|
||||
path = _compute_local_icon_path(conf_file).as_posix()
|
||||
|
||||
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
|
||||
path = compute_local_image_path(conf_file).as_posix()
|
||||
|
||||
else:
|
||||
raise core.EsphomeError(f"Unknown image source: {conf_file[CONF_SOURCE]}")
|
||||
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
file_contents = f.read()
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(f"Could not load image file {path}: {e}")
|
||||
|
||||
file_type = puremagic.from_string(file_contents, mime=True)
|
||||
async def write_image(config, all_frames=False):
|
||||
path = Path(config[CONF_FILE])
|
||||
if not path.is_file():
|
||||
raise core.EsphomeError(f"Could not load image file {path}")
|
||||
|
||||
resize = config.get(CONF_RESIZE)
|
||||
if "svg" in file_type:
|
||||
image = load_svg_image(file_contents, resize)
|
||||
else:
|
||||
image = Image.open(io.BytesIO(file_contents))
|
||||
if resize:
|
||||
image.thumbnail(resize)
|
||||
if is_svg_file(path):
|
||||
# Local import so use of non-SVG files needn't require cairosvg installed
|
||||
from cairosvg import svg2png
|
||||
|
||||
if not resize:
|
||||
resize = (None, None)
|
||||
with open(path, "rb") as file:
|
||||
image = svg2png(
|
||||
file_obj=file,
|
||||
output_width=resize[0],
|
||||
output_height=resize[1],
|
||||
)
|
||||
image = Image.open(io.BytesIO(image))
|
||||
width, height = image.size
|
||||
else:
|
||||
image = Image.open(path)
|
||||
width, height = image.size
|
||||
if resize:
|
||||
# Preserve aspect ratio
|
||||
new_width_max = min(width, resize[0])
|
||||
new_height_max = min(height, resize[1])
|
||||
ratio = min(new_width_max / width, new_height_max / height)
|
||||
width, height = int(width * ratio), int(height * ratio)
|
||||
|
||||
if CONF_RESIZE not in config and (width > 500 or height > 500):
|
||||
if not resize and (width > 500 or height > 500):
|
||||
_LOGGER.warning(
|
||||
'The image "%s" you requested is very big. Please consider'
|
||||
" using the resize parameter.",
|
||||
path,
|
||||
)
|
||||
|
||||
transparent = config[CONF_USE_TRANSPARENCY]
|
||||
|
||||
dither = (
|
||||
Image.Dither.NONE
|
||||
if config[CONF_DITHER] == "NONE"
|
||||
else Image.Dither.FLOYDSTEINBERG
|
||||
)
|
||||
if config[CONF_TYPE] == "GRAYSCALE":
|
||||
image = image.convert("LA", dither=dither)
|
||||
pixels = list(image.getdata())
|
||||
data = [0 for _ in range(height * width)]
|
||||
pos = 0
|
||||
for g, a in pixels:
|
||||
if transparent:
|
||||
if g == 1:
|
||||
g = 0
|
||||
if a < 0x80:
|
||||
g = 1
|
||||
type = config[CONF_TYPE]
|
||||
transparency = config[CONF_USE_TRANSPARENCY]
|
||||
invert_alpha = config[CONF_INVERT_ALPHA]
|
||||
frame_count = 1
|
||||
if all_frames:
|
||||
try:
|
||||
frame_count = image.n_frames
|
||||
except AttributeError:
|
||||
pass
|
||||
if frame_count <= 1:
|
||||
_LOGGER.warning("Image file %s has no animation frames", path)
|
||||
|
||||
data[pos] = g
|
||||
pos += 1
|
||||
total_rows = height * frame_count
|
||||
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
|
||||
for frame_index in range(frame_count):
|
||||
image.seek(frame_index)
|
||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
encoder.encode(pixels[row * width + col])
|
||||
encoder.end_row()
|
||||
|
||||
elif config[CONF_TYPE] == "RGBA":
|
||||
image = image.convert("RGBA")
|
||||
pixels = list(image.getdata())
|
||||
data = [0 for _ in range(height * width * 4)]
|
||||
pos = 0
|
||||
for r, g, b, a in pixels:
|
||||
data[pos] = r
|
||||
pos += 1
|
||||
data[pos] = g
|
||||
pos += 1
|
||||
data[pos] = b
|
||||
pos += 1
|
||||
data[pos] = a
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "RGB24":
|
||||
image = image.convert("RGBA")
|
||||
pixels = list(image.getdata())
|
||||
data = [0 for _ in range(height * width * 3)]
|
||||
pos = 0
|
||||
for r, g, b, a in pixels:
|
||||
if transparent:
|
||||
if r == 0 and g == 0 and b == 1:
|
||||
b = 0
|
||||
if a < 0x80:
|
||||
r = 0
|
||||
g = 0
|
||||
b = 1
|
||||
|
||||
data[pos] = r
|
||||
pos += 1
|
||||
data[pos] = g
|
||||
pos += 1
|
||||
data[pos] = b
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] in ["RGB565"]:
|
||||
image = image.convert("RGBA")
|
||||
pixels = list(image.getdata())
|
||||
bytes_per_pixel = 3 if transparent else 2
|
||||
data = [0 for _ in range(height * width * bytes_per_pixel)]
|
||||
pos = 0
|
||||
for r, g, b, a in pixels:
|
||||
R = r >> 3
|
||||
G = g >> 2
|
||||
B = b >> 3
|
||||
rgb = (R << 11) | (G << 5) | B
|
||||
data[pos] = rgb >> 8
|
||||
pos += 1
|
||||
data[pos] = rgb & 0xFF
|
||||
pos += 1
|
||||
if transparent:
|
||||
data[pos] = a
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
|
||||
if transparent:
|
||||
alpha = image.split()[-1]
|
||||
has_alpha = alpha.getextrema()[0] < 0xFF
|
||||
_LOGGER.debug("%s Has alpha: %s", config[CONF_ID], has_alpha)
|
||||
image = image.convert("1", dither=dither)
|
||||
width8 = ((width + 7) // 8) * 8
|
||||
data = [0 for _ in range(height * width8 // 8)]
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
if transparent and has_alpha:
|
||||
a = alpha.getpixel((x, y))
|
||||
if not a:
|
||||
continue
|
||||
elif image.getpixel((x, y)):
|
||||
continue
|
||||
pos = x + y * width8
|
||||
data[pos // 8] |= 0x80 >> (pos % 8)
|
||||
else:
|
||||
raise core.EsphomeError(
|
||||
f"Image f{config[CONF_ID]} has an unsupported type: {config[CONF_TYPE]}."
|
||||
)
|
||||
|
||||
rhs = [HexInt(x) for x in data]
|
||||
rhs = [HexInt(x) for x in encoder.data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]]
|
||||
image_type = get_image_type_enum(type)
|
||||
trans_value = get_transparency_enum(encoder.transparency)
|
||||
|
||||
return prog_arr, width, height, image_type, trans_value, frame_count
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
if isinstance(config, list):
|
||||
for entry in config:
|
||||
await to_code(entry)
|
||||
elif CONF_ID not in config:
|
||||
for entry in config.values():
|
||||
await to_code(entry)
|
||||
else:
|
||||
prog_arr, width, height, image_type, trans_value, _ = await write_image(config)
|
||||
cg.new_Pvariable(
|
||||
config[CONF_ID], prog_arr, width, height, image_type, trans_value
|
||||
)
|
||||
cg.add(var.set_transparency(transparent))
|
||||
|
@ -12,7 +12,7 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
if (this->get_binary_pixel_(img_x, img_y)) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_on);
|
||||
} else if (!this->transparent_) {
|
||||
} else if (!this->transparency_) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_off);
|
||||
}
|
||||
}
|
||||
@ -22,10 +22,27 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_grayscale_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
const uint32_t pos = (img_x + img_y * this->width_);
|
||||
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
||||
Color color = Color(gray, gray, gray, 0xFF);
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
if (gray == 1) {
|
||||
continue; // skip drawing
|
||||
}
|
||||
break;
|
||||
case TRANSPARENCY_ALPHA_CHANNEL: {
|
||||
auto on = (float) gray / 255.0f;
|
||||
auto off = 1.0f - on;
|
||||
// blend color_on and color_off
|
||||
color = Color(color_on.r * on + color_off.r * off, color_on.g * on + color_off.g * off,
|
||||
color_on.b * on + color_off.b * off, 0xFF);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -39,20 +56,10 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGB24:
|
||||
case IMAGE_TYPE_RGB:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_rgb24_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGBA:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_rgba_pixel_(img_x, img_y);
|
||||
auto color = this->get_rgb_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
@ -61,20 +68,20 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
||||
break;
|
||||
}
|
||||
}
|
||||
Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const {
|
||||
Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return color_off;
|
||||
switch (this->type_) {
|
||||
case IMAGE_TYPE_BINARY:
|
||||
return this->get_binary_pixel_(x, y) ? color_on : color_off;
|
||||
if (this->get_binary_pixel_(x, y))
|
||||
return color_on;
|
||||
return color_off;
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
return this->get_grayscale_pixel_(x, y);
|
||||
case IMAGE_TYPE_RGB565:
|
||||
return this->get_rgb565_pixel_(x, y);
|
||||
case IMAGE_TYPE_RGB24:
|
||||
return this->get_rgb24_pixel_(x, y);
|
||||
case IMAGE_TYPE_RGBA:
|
||||
return this->get_rgba_pixel_(x, y);
|
||||
case IMAGE_TYPE_RGB:
|
||||
return this->get_rgb_pixel_(x, y);
|
||||
default:
|
||||
return color_off;
|
||||
}
|
||||
@ -98,23 +105,40 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
|
||||
this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT;
|
||||
break;
|
||||
|
||||
case IMAGE_TYPE_RGB24:
|
||||
this->dsc_.header.cf = LV_IMG_CF_RGB888;
|
||||
case IMAGE_TYPE_RGB:
|
||||
#if LV_COLOR_DEPTH == 32
|
||||
switch (this->transparent_) {
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
|
||||
break;
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED;
|
||||
break;
|
||||
default:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
|
||||
break;
|
||||
}
|
||||
#else
|
||||
this->dsc_.header.cf =
|
||||
this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGBA8888 : LV_IMG_CF_RGB888;
|
||||
#endif
|
||||
break;
|
||||
|
||||
case IMAGE_TYPE_RGB565:
|
||||
#if LV_COLOR_DEPTH == 16
|
||||
this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_ALPHA : LV_IMG_CF_TRUE_COLOR;
|
||||
#else
|
||||
this->dsc_.header.cf = LV_IMG_CF_RGB565;
|
||||
#endif
|
||||
break;
|
||||
|
||||
case IMAGE_TYPE_RGBA:
|
||||
#if LV_COLOR_DEPTH == 32
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
|
||||
#else
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
|
||||
break;
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED;
|
||||
break;
|
||||
default:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
|
||||
break;
|
||||
}
|
||||
#else
|
||||
this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@ -128,51 +152,81 @@ bool Image::get_binary_pixel_(int x, int y) const {
|
||||
const uint32_t pos = x + y * width_8;
|
||||
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
|
||||
}
|
||||
Color Image::get_rgba_pixel_(int x, int y) const {
|
||||
const uint32_t pos = (x + y * this->width_) * 4;
|
||||
return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
|
||||
progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3));
|
||||
}
|
||||
Color Image::get_rgb24_pixel_(int x, int y) const {
|
||||
const uint32_t pos = (x + y * this->width_) * 3;
|
||||
Color Image::get_rgb_pixel_(int x, int y) const {
|
||||
const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
|
||||
Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
|
||||
progmem_read_byte(this->data_start_ + pos + 2));
|
||||
if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
|
||||
// (0, 0, 1) has been defined as transparent color for non-alpha images.
|
||||
// putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if)
|
||||
progmem_read_byte(this->data_start_ + pos + 2), 0xFF);
|
||||
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
if (color.g == 1 && color.r == 0 && color.b == 0) {
|
||||
// (0, 1, 0) has been defined as transparent color for non-alpha images.
|
||||
color.w = 0;
|
||||
} else {
|
||||
color.w = 0xFF;
|
||||
}
|
||||
break;
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
color.w = progmem_read_byte(this->data_start_ + (pos + 3));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
Color Image::get_rgb565_pixel_(int x, int y) const {
|
||||
const uint8_t *pos = this->data_start_;
|
||||
if (this->transparent_) {
|
||||
pos += (x + y * this->width_) * 3;
|
||||
} else {
|
||||
pos += (x + y * this->width_) * 2;
|
||||
}
|
||||
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
|
||||
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF;
|
||||
Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a);
|
||||
return color;
|
||||
auto a = 0xFF;
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
a = progmem_read_byte(pos + 2);
|
||||
break;
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
if (rgb565 == 0x0020)
|
||||
a = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a);
|
||||
}
|
||||
|
||||
Color Image::get_grayscale_pixel_(int x, int y) const {
|
||||
const uint32_t pos = (x + y * this->width_);
|
||||
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
||||
uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF;
|
||||
return Color(gray, gray, gray, alpha);
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
if (gray == 1)
|
||||
return Color(0, 0, 0, 0);
|
||||
return Color(gray, gray, gray, 0xFF);
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
return Color(0, 0, 0, gray);
|
||||
default:
|
||||
return Color(gray, gray, gray, 0xFF);
|
||||
}
|
||||
}
|
||||
int Image::get_width() const { return this->width_; }
|
||||
int Image::get_height() const { return this->height_; }
|
||||
ImageType Image::get_type() const { return this->type_; }
|
||||
Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
|
||||
: width_(width), height_(height), type_(type), data_start_(data_start) {}
|
||||
Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency)
|
||||
: width_(width), height_(height), type_(type), data_start_(data_start), transparency_(transparency) {
|
||||
switch (this->type_) {
|
||||
case IMAGE_TYPE_BINARY:
|
||||
this->bpp_ = 1;
|
||||
break;
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
this->bpp_ = 8;
|
||||
break;
|
||||
case IMAGE_TYPE_RGB565:
|
||||
this->bpp_ = transparency == TRANSPARENCY_ALPHA_CHANNEL ? 24 : 16;
|
||||
break;
|
||||
case IMAGE_TYPE_RGB:
|
||||
this->bpp_ = this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? 32 : 24;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace image
|
||||
} // namespace esphome
|
||||
|
@ -12,51 +12,40 @@ namespace image {
|
||||
enum ImageType {
|
||||
IMAGE_TYPE_BINARY = 0,
|
||||
IMAGE_TYPE_GRAYSCALE = 1,
|
||||
IMAGE_TYPE_RGB24 = 2,
|
||||
IMAGE_TYPE_RGB = 2,
|
||||
IMAGE_TYPE_RGB565 = 3,
|
||||
IMAGE_TYPE_RGBA = 4,
|
||||
};
|
||||
|
||||
enum Transparency {
|
||||
TRANSPARENCY_OPAQUE = 0,
|
||||
TRANSPARENCY_CHROMA_KEY = 1,
|
||||
TRANSPARENCY_ALPHA_CHANNEL = 2,
|
||||
};
|
||||
|
||||
class Image : public display::BaseImage {
|
||||
public:
|
||||
Image(const uint8_t *data_start, int width, int height, ImageType type);
|
||||
Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency);
|
||||
Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const;
|
||||
int get_width() const override;
|
||||
int get_height() const override;
|
||||
const uint8_t *get_data_start() const { return this->data_start_; }
|
||||
ImageType get_type() const;
|
||||
|
||||
int get_bpp() const {
|
||||
switch (this->type_) {
|
||||
case IMAGE_TYPE_BINARY:
|
||||
return 1;
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
return 8;
|
||||
case IMAGE_TYPE_RGB565:
|
||||
return this->transparent_ ? 24 : 16;
|
||||
case IMAGE_TYPE_RGB24:
|
||||
return 24;
|
||||
case IMAGE_TYPE_RGBA:
|
||||
return 32;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int get_bpp() const { return this->bpp_; }
|
||||
|
||||
/// Return the stride of the image in bytes, that is, the distance in bytes
|
||||
/// between two consecutive rows of pixels.
|
||||
uint32_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; }
|
||||
size_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; }
|
||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||
|
||||
void set_transparency(bool transparent) { transparent_ = transparent; }
|
||||
bool has_transparency() const { return transparent_; }
|
||||
bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; }
|
||||
|
||||
#ifdef USE_LVGL
|
||||
lv_img_dsc_t *get_lv_img_dsc();
|
||||
#endif
|
||||
protected:
|
||||
bool get_binary_pixel_(int x, int y) const;
|
||||
Color get_rgb24_pixel_(int x, int y) const;
|
||||
Color get_rgba_pixel_(int x, int y) const;
|
||||
Color get_rgb_pixel_(int x, int y) const;
|
||||
Color get_rgb565_pixel_(int x, int y) const;
|
||||
Color get_grayscale_pixel_(int x, int y) const;
|
||||
|
||||
@ -64,7 +53,9 @@ class Image : public display::BaseImage {
|
||||
int height_;
|
||||
ImageType type_;
|
||||
const uint8_t *data_start_;
|
||||
bool transparent_;
|
||||
Transparency transparency_;
|
||||
size_t bpp_{};
|
||||
size_t stride_{};
|
||||
#ifdef USE_LVGL
|
||||
lv_img_dsc_t dsc_{};
|
||||
#endif
|
||||
|
@ -1,45 +1,27 @@
|
||||
#include "json_util.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <Esp.h>
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_heap_caps.h>
|
||||
#endif
|
||||
#ifdef USE_RP2040
|
||||
#include <Arduino.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace json {
|
||||
|
||||
static const char *const TAG = "json";
|
||||
|
||||
static std::vector<char> global_json_build_buffer; // NOLINT
|
||||
static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
|
||||
|
||||
std::string build_json(const json_build_t &f) {
|
||||
// Here we are allocating up to 5kb of memory,
|
||||
// with the heap size minus 2kb to be safe if less than 5kb
|
||||
// as we can not have a true dynamic sized document.
|
||||
// The excess memory is freed below with `shrinkToFit()`
|
||||
#ifdef USE_ESP8266
|
||||
const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance)
|
||||
#elif defined(USE_ESP32)
|
||||
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
|
||||
#elif defined(USE_RP2040)
|
||||
const size_t free_heap = rp2040.getFreeHeap();
|
||||
#elif defined(USE_LIBRETINY)
|
||||
const size_t free_heap = lt_heap_get_free();
|
||||
#endif
|
||||
|
||||
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
||||
size_t request_size = std::min(free_heap, (size_t) 512);
|
||||
while (true) {
|
||||
ESP_LOGV(TAG, "Attempting to allocate %u bytes for JSON serialization", request_size);
|
||||
ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size);
|
||||
DynamicJsonDocument json_document(request_size);
|
||||
if (json_document.capacity() == 0) {
|
||||
ESP_LOGE(TAG,
|
||||
"Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
|
||||
"Could not allocate memory for JSON document! Requested %zu bytes, largest free heap block: %zu bytes",
|
||||
request_size, free_heap);
|
||||
return "{}";
|
||||
}
|
||||
@ -47,7 +29,7 @@ std::string build_json(const json_build_t &f) {
|
||||
f(root);
|
||||
if (json_document.overflowed()) {
|
||||
if (request_size == free_heap) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %u bytes",
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %zu bytes",
|
||||
free_heap);
|
||||
return "{}";
|
||||
}
|
||||
@ -55,7 +37,7 @@ std::string build_json(const json_build_t &f) {
|
||||
continue;
|
||||
}
|
||||
json_document.shrinkToFit();
|
||||
ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
|
||||
ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity());
|
||||
std::string output;
|
||||
serializeJson(json_document, output);
|
||||
return output;
|
||||
@ -67,20 +49,12 @@ bool parse_json(const std::string &data, const json_parse_t &f) {
|
||||
// with the heap size minus 2kb to be safe if less than that
|
||||
// as we can not have a true dynamic sized document.
|
||||
// The excess memory is freed below with `shrinkToFit()`
|
||||
#ifdef USE_ESP8266
|
||||
const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance)
|
||||
#elif defined(USE_ESP32)
|
||||
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
|
||||
#elif defined(USE_RP2040)
|
||||
const size_t free_heap = rp2040.getFreeHeap();
|
||||
#elif defined(USE_LIBRETINY)
|
||||
const size_t free_heap = lt_heap_get_free();
|
||||
#endif
|
||||
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
||||
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));
|
||||
while (true) {
|
||||
DynamicJsonDocument json_document(request_size);
|
||||
if (json_document.capacity() == 0) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size,
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %zu bytes, free heap: %zu", request_size,
|
||||
free_heap);
|
||||
return false;
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str());
|
||||
return true;
|
||||
}
|
||||
stored_data.data.reserve(kv.value_len);
|
||||
stored_data.data.resize(kv.value_len);
|
||||
fdb_blob_make(&blob, stored_data.data.data(), kv.value_len);
|
||||
size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob);
|
||||
if (actual_len != kv.value_len) {
|
||||
|
@ -36,7 +36,7 @@ inline static uint8_t to_uint8_scale(float x) { return static_cast<uint8_t>(roun
|
||||
* range as set in the traits, so the output needs to do this.
|
||||
*
|
||||
* For COLD_WARM_WHITE capability:
|
||||
* - cold_white, warm_white: The brightness of the cald and warm white channels of the light.
|
||||
* - cold_white, warm_white: The brightness of the light's cold and warm white channels.
|
||||
*
|
||||
* All values (except color temperature) are represented using floats in the range 0.0 (off) to 1.0 (on), and are
|
||||
* automatically clamped to this range. Properties not used in the current color mode can still have (invalid) values
|
||||
|
@ -23,7 +23,7 @@ from esphome.helpers import write_file_if_changed
|
||||
|
||||
from . import defines as df, helpers, lv_validation as lvalid
|
||||
from .automation import disp_update, focused_widgets, update_to_code
|
||||
from .defines import add_define
|
||||
from .defines import CONF_DRAW_ROUNDING, add_define
|
||||
from .encoders import (
|
||||
ENCODERS_CONFIG,
|
||||
encoders_to_code,
|
||||
@ -197,14 +197,18 @@ def final_validation(configs):
|
||||
for display_id in config[df.CONF_DISPLAYS]:
|
||||
path = global_config.get_path_for_id(display_id)[:-1]
|
||||
display = global_config.get_config_for_path(path)
|
||||
if CONF_LAMBDA in display:
|
||||
if CONF_LAMBDA in display or CONF_PAGES in display:
|
||||
raise cv.Invalid(
|
||||
"Using lambda: in display config not compatible with LVGL"
|
||||
"Using lambda: or pages: in display config is not compatible with LVGL"
|
||||
)
|
||||
if display[CONF_AUTO_CLEAR_ENABLED]:
|
||||
if display.get(CONF_AUTO_CLEAR_ENABLED) is True:
|
||||
raise cv.Invalid(
|
||||
"Using auto_clear_enabled: true in display config not compatible with LVGL"
|
||||
)
|
||||
if draw_rounding := display.get(CONF_DRAW_ROUNDING):
|
||||
config[CONF_DRAW_ROUNDING] = max(
|
||||
draw_rounding, config[CONF_DRAW_ROUNDING]
|
||||
)
|
||||
buffer_frac = config[CONF_BUFFER_SIZE]
|
||||
if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config:
|
||||
LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
|
||||
|
@ -4,24 +4,27 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT
|
||||
from esphome.cpp_generator import get_variable
|
||||
from esphome.cpp_generator import TemplateArguments, get_variable
|
||||
from esphome.cpp_types import nullptr
|
||||
|
||||
from .defines import (
|
||||
CONF_DISP_BG_COLOR,
|
||||
CONF_DISP_BG_IMAGE,
|
||||
CONF_DISP_BG_OPA,
|
||||
CONF_EDITING,
|
||||
CONF_FREEZE,
|
||||
CONF_LVGL_ID,
|
||||
CONF_SHOW_SNOW,
|
||||
PARTS,
|
||||
literal,
|
||||
)
|
||||
from .lv_validation import lv_bool, lv_color, lv_image
|
||||
from .lv_validation import lv_bool, lv_color, lv_image, opacity
|
||||
from .lvcode import (
|
||||
LVGL_COMP_ARG,
|
||||
UPDATE_EVENT,
|
||||
LambdaContext,
|
||||
LocalVariable,
|
||||
LvglComponent,
|
||||
ReturnStatement,
|
||||
add_line_marks,
|
||||
lv,
|
||||
@ -31,7 +34,7 @@ from .lvcode import (
|
||||
lvgl_comp,
|
||||
static_cast,
|
||||
)
|
||||
from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA
|
||||
from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA, base_update_schema
|
||||
from .types import (
|
||||
LV_STATE,
|
||||
LvglAction,
|
||||
@ -39,6 +42,7 @@ from .types import (
|
||||
ObjUpdateAction,
|
||||
lv_disp_t,
|
||||
lv_group_t,
|
||||
lv_obj_base_t,
|
||||
lv_obj_t,
|
||||
lv_pseudo_button_t,
|
||||
)
|
||||
@ -92,7 +96,11 @@ async def lvgl_is_paused(config, condition_id, template_arg, args):
|
||||
lvgl = config[CONF_LVGL_ID]
|
||||
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
|
||||
lv_add(ReturnStatement(lvgl_comp.is_paused()))
|
||||
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
|
||||
var = cg.new_Pvariable(
|
||||
condition_id,
|
||||
TemplateArguments(LvglComponent, *template_arg),
|
||||
await context.get_lambda(),
|
||||
)
|
||||
await cg.register_parented(var, lvgl)
|
||||
return var
|
||||
|
||||
@ -113,19 +121,32 @@ async def lvgl_is_idle(config, condition_id, template_arg, args):
|
||||
timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32)
|
||||
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
|
||||
lv_add(ReturnStatement(lvgl_comp.is_idle(timeout)))
|
||||
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
|
||||
var = cg.new_Pvariable(
|
||||
condition_id,
|
||||
TemplateArguments(LvglComponent, *template_arg),
|
||||
await context.get_lambda(),
|
||||
)
|
||||
await cg.register_parented(var, lvgl)
|
||||
return var
|
||||
|
||||
|
||||
async def disp_update(disp, config: dict):
|
||||
if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config:
|
||||
if (
|
||||
CONF_DISP_BG_COLOR not in config
|
||||
and CONF_DISP_BG_IMAGE not in config
|
||||
and CONF_DISP_BG_OPA not in config
|
||||
):
|
||||
return
|
||||
with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp:
|
||||
if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None:
|
||||
lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
|
||||
if bg_image := config.get(CONF_DISP_BG_IMAGE):
|
||||
if bg_image == "none":
|
||||
lv.disp_set_bg_image(disp_temp, static_cast("void *", "nullptr"))
|
||||
else:
|
||||
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))
|
||||
if (bg_opa := config.get(CONF_DISP_BG_OPA)) is not None:
|
||||
lv.disp_set_bg_opa(disp_temp, await opacity.process(bg_opa))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@ -317,3 +338,14 @@ async def widget_focus(config, action_id, template_arg, args):
|
||||
lv.group_focus_freeze(group, True)
|
||||
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.widget.update", ObjUpdateAction, base_update_schema(lv_obj_base_t, PARTS)
|
||||
)
|
||||
async def obj_update_to_code(config, action_id, template_arg, args):
|
||||
async def do_update(widget: Widget):
|
||||
await set_obj_properties(widget, config)
|
||||
|
||||
widgets = await get_widgets(config[CONF_ID])
|
||||
return await action_to_code(widgets, do_update, action_id, template_arg, args)
|
||||
|
@ -168,6 +168,7 @@ LV_EVENT_MAP = {
|
||||
"READY": "READY",
|
||||
"CANCEL": "CANCEL",
|
||||
"ALL_EVENTS": "ALL",
|
||||
"CHANGE": "VALUE_CHANGED",
|
||||
}
|
||||
|
||||
LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP)
|
||||
@ -214,7 +215,7 @@ LV_LONG_MODES = LvConstant(
|
||||
)
|
||||
|
||||
STATES = (
|
||||
"default",
|
||||
# default state not included here
|
||||
"checked",
|
||||
"focused",
|
||||
"focus_key",
|
||||
@ -402,6 +403,7 @@ CONF_COLUMN = "column"
|
||||
CONF_DIGITS = "digits"
|
||||
CONF_DISP_BG_COLOR = "disp_bg_color"
|
||||
CONF_DISP_BG_IMAGE = "disp_bg_image"
|
||||
CONF_DISP_BG_OPA = "disp_bg_opa"
|
||||
CONF_BODY = "body"
|
||||
CONF_BUTTONS = "buttons"
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
|
@ -119,6 +119,7 @@ void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_ev
|
||||
}
|
||||
void LvglComponent::add_page(LvPageType *page) {
|
||||
this->pages_.push_back(page);
|
||||
page->set_parent(this);
|
||||
page->setup(this->pages_.size() - 1);
|
||||
}
|
||||
void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) {
|
||||
@ -143,6 +144,8 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
|
||||
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
|
||||
this->show_page(this->current_page_, anim, time);
|
||||
}
|
||||
size_t LvglComponent::get_current_page() const { return this->current_page_; }
|
||||
bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; }
|
||||
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
|
||||
auto width = lv_area_get_width(area);
|
||||
auto height = lv_area_get_height(area);
|
||||
@ -498,9 +501,7 @@ size_t lv_millis(void) { return esphome::millis(); }
|
||||
void *lv_custom_mem_alloc(size_t size) {
|
||||
auto *ptr = malloc(size); // NOLINT
|
||||
if (ptr == nullptr) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
|
||||
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
|
||||
#endif
|
||||
ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
@ -517,30 +518,22 @@ void *lv_custom_mem_alloc(size_t size) {
|
||||
ptr = heap_caps_malloc(size, cap_bits);
|
||||
}
|
||||
if (ptr == nullptr) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
|
||||
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
|
||||
#endif
|
||||
ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
|
||||
return nullptr;
|
||||
}
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
esphome::ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr);
|
||||
#endif
|
||||
ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void lv_custom_mem_free(void *ptr) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
esphome::ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
|
||||
#endif
|
||||
ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
|
||||
if (ptr == nullptr)
|
||||
return;
|
||||
heap_caps_free(ptr);
|
||||
}
|
||||
|
||||
void *lv_custom_mem_realloc(void *ptr, size_t size) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
esphome::ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
|
||||
#endif
|
||||
ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
|
||||
return heap_caps_realloc(ptr, size, cap_bits);
|
||||
}
|
||||
#endif
|
||||
|
@ -59,6 +59,16 @@ inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) {
|
||||
inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image) {
|
||||
lv_disp_set_bg_image(disp, image->get_lv_img_dsc());
|
||||
}
|
||||
|
||||
inline void lv_obj_set_style_bg_img_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) {
|
||||
lv_obj_set_style_bg_img_src(obj, image->get_lv_img_dsc(), selector);
|
||||
}
|
||||
#ifdef USE_LVGL_METER
|
||||
inline lv_meter_indicator_t *lv_meter_add_needle_img(lv_obj_t *obj, lv_meter_scale_t *scale, esphome::image::Image *src,
|
||||
lv_coord_t pivot_x, lv_coord_t pivot_y) {
|
||||
return lv_meter_add_needle_img(obj, scale, src->get_lv_img_dsc(), pivot_x, pivot_y);
|
||||
}
|
||||
#endif // USE_LVGL_METER
|
||||
#endif // USE_LVGL_IMAGE
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) {
|
||||
@ -84,7 +94,9 @@ class LvCompound {
|
||||
lv_obj_t *obj{};
|
||||
};
|
||||
|
||||
class LvPageType {
|
||||
class LvglComponent;
|
||||
|
||||
class LvPageType : public Parented<LvglComponent> {
|
||||
public:
|
||||
LvPageType(bool skip) : skip(skip) {}
|
||||
|
||||
@ -92,6 +104,9 @@ class LvPageType {
|
||||
this->index = index;
|
||||
this->obj = lv_obj_create(nullptr);
|
||||
}
|
||||
|
||||
bool is_showing() const;
|
||||
|
||||
lv_obj_t *obj{};
|
||||
size_t index{};
|
||||
bool skip;
|
||||
@ -178,6 +193,7 @@ class LvglComponent : public PollingComponent {
|
||||
void show_next_page(lv_scr_load_anim_t anim, uint32_t time);
|
||||
void show_prev_page(lv_scr_load_anim_t anim, uint32_t time);
|
||||
void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; }
|
||||
size_t get_current_page() const;
|
||||
void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); }
|
||||
void restore_focus_mark(lv_group_t *group) {
|
||||
auto *mark = this->focus_marks_[group];
|
||||
@ -241,14 +257,13 @@ template<typename... Ts> class LvglAction : public Action<Ts...>, public Parente
|
||||
std::function<void(LvglComponent *)> action_{};
|
||||
};
|
||||
|
||||
template<typename... Ts> class LvglCondition : public Condition<Ts...>, public Parented<LvglComponent> {
|
||||
template<typename Tc, typename... Ts> class LvglCondition : public Condition<Ts...>, public Parented<Tc> {
|
||||
public:
|
||||
LvglCondition(std::function<bool(LvglComponent *)> &&condition_lambda)
|
||||
: condition_lambda_(std::move(condition_lambda)) {}
|
||||
LvglCondition(std::function<bool(Tc *)> &&condition_lambda) : condition_lambda_(std::move(condition_lambda)) {}
|
||||
bool check(Ts... x) override { return this->condition_lambda_(this->parent_); }
|
||||
|
||||
protected:
|
||||
std::function<bool(LvglComponent *)> condition_lambda_{};
|
||||
std::function<bool(Tc *)> condition_lambda_{};
|
||||
};
|
||||
|
||||
#ifdef USE_LVGL_TOUCHSCREEN
|
||||
|
@ -19,7 +19,7 @@ from esphome.schema_extractors import SCHEMA_EXTRACT
|
||||
from . import defines as df, lv_validation as lvalid
|
||||
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
|
||||
from .helpers import add_lv_use, requires_component, validate_printf
|
||||
from .lv_validation import lv_color, lv_font, lv_gradient, lv_image
|
||||
from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity
|
||||
from .lvcode import LvglComponent, lv_event_t_ptr
|
||||
from .types import (
|
||||
LVEncoderListener,
|
||||
@ -199,13 +199,12 @@ FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FL
|
||||
FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of)
|
||||
|
||||
|
||||
def part_schema(widget_type: WidgetType):
|
||||
def part_schema(parts):
|
||||
"""
|
||||
Generate a schema for the various parts (e.g. main:, indicator:) of a widget type
|
||||
:param widget_type: The type of widget to generate for
|
||||
:return:
|
||||
:param parts: The parts to include in the schema
|
||||
:return: The schema
|
||||
"""
|
||||
parts = widget_type.parts
|
||||
return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend(
|
||||
STATE_SCHEMA
|
||||
)
|
||||
@ -228,9 +227,15 @@ def automation_schema(typ: LvType):
|
||||
}
|
||||
|
||||
|
||||
def create_modify_schema(widget_type):
|
||||
def base_update_schema(widget_type, parts):
|
||||
"""
|
||||
Create a schema for updating a widgets style properties, states and flags
|
||||
:param widget_type: The type of the ID
|
||||
:param parts: The allowable parts to specify
|
||||
:return:
|
||||
"""
|
||||
return (
|
||||
part_schema(widget_type)
|
||||
part_schema(parts)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.ensure_list(
|
||||
@ -245,7 +250,12 @@ def create_modify_schema(widget_type):
|
||||
}
|
||||
)
|
||||
.extend(FLAG_SCHEMA)
|
||||
.extend(widget_type.modify_schema)
|
||||
)
|
||||
|
||||
|
||||
def create_modify_schema(widget_type):
|
||||
return base_update_schema(widget_type.w_type, widget_type.parts).extend(
|
||||
widget_type.modify_schema
|
||||
)
|
||||
|
||||
|
||||
@ -256,7 +266,7 @@ def obj_schema(widget_type: WidgetType):
|
||||
:return:
|
||||
"""
|
||||
return (
|
||||
part_schema(widget_type)
|
||||
part_schema(widget_type.parts)
|
||||
.extend(FLAG_SCHEMA)
|
||||
.extend(LAYOUT_SCHEMA)
|
||||
.extend(ALIGN_TO_SCHEMA)
|
||||
@ -341,11 +351,13 @@ FLEX_OBJ_SCHEMA = {
|
||||
cv.Optional(df.CONF_FLEX_GROW): cv.int_,
|
||||
}
|
||||
|
||||
|
||||
DISP_BG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image,
|
||||
cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any(
|
||||
cv.one_of("none", lower=True), lv_image
|
||||
),
|
||||
cv.Optional(df.CONF_DISP_BG_COLOR): lv_color,
|
||||
cv.Optional(df.CONF_DISP_BG_OPA): opacity,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -37,7 +37,7 @@ DROPDOWN_BASE_SCHEMA = cv.Schema(
|
||||
cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int,
|
||||
cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
|
||||
cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of,
|
||||
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec),
|
||||
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -79,7 +79,7 @@ class ImgType(WidgetType):
|
||||
if CONF_ANTIALIAS in config:
|
||||
lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS])
|
||||
if mode := config.get(CONF_MODE):
|
||||
lv.img_set_mode(w.obj, mode)
|
||||
await w.set_property("size_mode", mode)
|
||||
|
||||
|
||||
img_spec = ImgType()
|
||||
|
@ -16,6 +16,11 @@ KEYBOARD_SCHEMA = {
|
||||
cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t),
|
||||
}
|
||||
|
||||
KEYBOARD_MODIFY_SCHEMA = {
|
||||
cv.Optional(CONF_MODE): KEYBOARD_MODES.one_of,
|
||||
cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t),
|
||||
}
|
||||
|
||||
lv_keyboard_t = LvType(
|
||||
"LvKeyboardType",
|
||||
parents=(KeyProvider, LvCompound),
|
||||
@ -32,6 +37,7 @@ class KeyboardType(WidgetType):
|
||||
lv_keyboard_t,
|
||||
(CONF_MAIN, CONF_ITEMS),
|
||||
KEYBOARD_SCHEMA,
|
||||
modify_schema=KEYBOARD_MODIFY_SCHEMA,
|
||||
)
|
||||
|
||||
def get_uses(self):
|
||||
@ -41,7 +47,8 @@ class KeyboardType(WidgetType):
|
||||
lvgl_components_required.add("KEY_LISTENER")
|
||||
lvgl_components_required.add(CONF_KEYBOARD)
|
||||
add_lv_use("btnmatrix")
|
||||
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(config[CONF_MODE]))
|
||||
if mode := config.get(CONF_MODE):
|
||||
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode))
|
||||
if ta := await get_widgets(config, CONF_TEXTAREA):
|
||||
await w.set_property(CONF_TEXTAREA, ta[0].obj)
|
||||
|
||||
|
@ -27,7 +27,7 @@ from ..defines import (
|
||||
CONF_START_VALUE,
|
||||
CONF_TICKS,
|
||||
)
|
||||
from ..helpers import add_lv_use
|
||||
from ..helpers import add_lv_use, lvgl_components_required
|
||||
from ..lv_validation import (
|
||||
angle,
|
||||
get_end_value,
|
||||
@ -182,6 +182,7 @@ class MeterType(WidgetType):
|
||||
async def to_code(self, w: Widget, config):
|
||||
"""For a meter object, create and set parameters"""
|
||||
|
||||
lvgl_components_required.add(CONF_METER)
|
||||
var = w.obj
|
||||
for scale_conf in config.get(CONF_SCALES, ()):
|
||||
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
||||
|
@ -51,7 +51,7 @@ MSGBOX_SCHEMA = container_schema(
|
||||
cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA,
|
||||
cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA,
|
||||
cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA),
|
||||
cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec),
|
||||
cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec.parts),
|
||||
cv.Optional(CONF_CLOSE_BUTTON, default=True): lv_bool,
|
||||
cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr),
|
||||
}
|
||||
|
@ -1,9 +1,5 @@
|
||||
from esphome import automation
|
||||
|
||||
from ..automation import update_to_code
|
||||
from ..defines import CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR
|
||||
from ..schemas import create_modify_schema
|
||||
from ..types import ObjUpdateAction, WidgetType, lv_obj_t
|
||||
from ..types import WidgetType, lv_obj_t
|
||||
|
||||
|
||||
class ObjType(WidgetType):
|
||||
@ -21,10 +17,3 @@ class ObjType(WidgetType):
|
||||
|
||||
|
||||
obj_spec = ObjType()
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.widget.update", ObjUpdateAction, create_modify_schema(obj_spec)
|
||||
)
|
||||
async def obj_update_to_code(config, action_id, template_arg, args):
|
||||
return await update_to_code(config, action_id, template_arg, args)
|
||||
|
@ -2,6 +2,7 @@ from esphome import automation, codegen as cg
|
||||
from esphome.automation import Trigger
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME, CONF_TRIGGER_ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArguments
|
||||
|
||||
from ..defines import (
|
||||
CONF_ANIMATION,
|
||||
@ -17,18 +18,28 @@ from ..lvcode import (
|
||||
EVENT_ARG,
|
||||
LVGL_COMP_ARG,
|
||||
LambdaContext,
|
||||
ReturnStatement,
|
||||
add_line_marks,
|
||||
lv_add,
|
||||
lvgl_comp,
|
||||
lvgl_static,
|
||||
)
|
||||
from ..schemas import LVGL_SCHEMA
|
||||
from ..types import LvglAction, lv_page_t
|
||||
from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
|
||||
from ..types import LvglAction, LvglCondition, lv_page_t
|
||||
from . import (
|
||||
Widget,
|
||||
WidgetType,
|
||||
add_widgets,
|
||||
get_widgets,
|
||||
set_obj_properties,
|
||||
wait_for_widgets,
|
||||
)
|
||||
|
||||
CONF_ON_LOAD = "on_load"
|
||||
CONF_ON_UNLOAD = "on_unload"
|
||||
|
||||
PAGE_ARG = "_page"
|
||||
|
||||
PAGE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SKIP, default=False): lv_bool,
|
||||
@ -86,6 +97,30 @@ async def page_next_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"lvgl.page.is_showing",
|
||||
LvglCondition,
|
||||
cv.maybe_simple_value(
|
||||
cv.Schema({cv.Required(CONF_ID): cv.use_id(lv_page_t)}),
|
||||
key=CONF_ID,
|
||||
),
|
||||
)
|
||||
async def page_is_showing_to_code(config, condition_id, template_arg, args):
|
||||
await wait_for_widgets()
|
||||
page = await cg.get_variable(config[CONF_ID])
|
||||
async with LambdaContext(
|
||||
[(lv_page_t.operator("ptr"), PAGE_ARG)], return_type=cg.bool_
|
||||
) as context:
|
||||
lv_add(ReturnStatement(MockObj(PAGE_ARG, "->").is_showing()))
|
||||
var = cg.new_Pvariable(
|
||||
condition_id,
|
||||
TemplateArguments(lv_page_t, *template_arg),
|
||||
await context.get_lambda(),
|
||||
)
|
||||
await cg.register_parented(var, page)
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.page.previous",
|
||||
LvglAction,
|
||||
|
@ -38,7 +38,7 @@ TABVIEW_SCHEMA = cv.Schema(
|
||||
},
|
||||
)
|
||||
),
|
||||
cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec),
|
||||
cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec.parts),
|
||||
cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of,
|
||||
cv.Optional(CONF_SIZE, default="10%"): size,
|
||||
}
|
||||
|
@ -11,16 +11,18 @@ void MicroNovaSwitch::write_state(bool state) {
|
||||
if (this->micronova_->get_current_stove_state() == 0) {
|
||||
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_);
|
||||
this->publish_state(true);
|
||||
} else
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", micronova_->get_current_stove_state());
|
||||
}
|
||||
} else {
|
||||
// don't send power-off when status is Off or Final cleaning
|
||||
if (this->micronova_->get_current_stove_state() != 0 && micronova_->get_current_stove_state() != 6) {
|
||||
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_);
|
||||
this->publish_state(false);
|
||||
} else
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", micronova_->get_current_stove_state());
|
||||
}
|
||||
}
|
||||
this->micronova_->update();
|
||||
break;
|
||||
|
||||
|
@ -19,10 +19,12 @@ template<typename... Ts> class MideaActionBase : public Action<Ts...> {
|
||||
|
||||
template<typename... Ts> class FollowMeAction : public MideaActionBase<Ts...> {
|
||||
TEMPLATABLE_VALUE(float, temperature)
|
||||
TEMPLATABLE_VALUE(bool, use_fahrenheit)
|
||||
TEMPLATABLE_VALUE(bool, beeper)
|
||||
|
||||
void play(Ts... x) override {
|
||||
this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...));
|
||||
this->parent_->do_follow_me(this->temperature_.value(x...), this->use_fahrenheit_.value(x...),
|
||||
this->beeper_.value(x...));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "air_conditioner.h"
|
||||
#include "ac_adapter.h"
|
||||
#include <cmath>
|
||||
@ -121,7 +122,7 @@ void AirConditioner::dump_config() {
|
||||
|
||||
/* ACTIONS */
|
||||
|
||||
void AirConditioner::do_follow_me(float temperature, bool beeper) {
|
||||
void AirConditioner::do_follow_me(float temperature, bool use_fahrenheit, bool beeper) {
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
// Check if temperature is finite (not NaN or infinite)
|
||||
if (!std::isfinite(temperature)) {
|
||||
@ -131,13 +132,14 @@ void AirConditioner::do_follow_me(float temperature, bool beeper) {
|
||||
|
||||
// Round and convert temperature to long, then clamp and convert it to uint8_t
|
||||
uint8_t temp_uint8 =
|
||||
static_cast<uint8_t>(std::max(0L, std::min(static_cast<long>(UINT8_MAX), std::lroundf(temperature))));
|
||||
static_cast<uint8_t>(esphome::clamp<long>(std::lroundf(temperature), 0L, static_cast<long>(UINT8_MAX)));
|
||||
|
||||
ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %f °C, rounded to: %u °C", temperature,
|
||||
temp_uint8);
|
||||
char temp_symbol = use_fahrenheit ? 'F' : 'C';
|
||||
ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %.5f °%c, rounded to: %u °%c", temperature,
|
||||
temp_symbol, temp_uint8, temp_symbol);
|
||||
|
||||
// Create and transmit the data
|
||||
IrFollowMeData data(temp_uint8, beeper);
|
||||
IrFollowMeData data(temp_uint8, use_fahrenheit, beeper);
|
||||
this->transmitter_.transmit(data);
|
||||
#else
|
||||
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
|
||||
|
@ -32,7 +32,7 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
|
||||
/* ### ACTIONS ### */
|
||||
/* ############### */
|
||||
|
||||
void do_follow_me(float temperature, bool beeper = false);
|
||||
void do_follow_me(float temperature, bool use_fahrenheit, bool beeper = false);
|
||||
void do_display_toggle();
|
||||
void do_swing_step();
|
||||
void do_beeper_on() { this->set_beeper_feedback(true); }
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user