Compare commits

..
Author SHA1 Message Date
J. Nick Koston 29bc2c1275 [sendspin] Assert the publish follows the hub call directly 2026-10-26 08:03:12 -10:00
J. Nick Koston e7dd4f06ea [sendspin] Pass True to the enabled switch define and trim a test docstring 2026-10-26 07:31:15 -10:00
J. Nick Koston eabc7df6fc [sendspin] Use an Apply switch for the enabled switch 2026-10-26 07:27:17 -10:00
J. Nick Koston 650db15e2b [sendspin] Test that the enabled switch, not codegen, answers the hub 2026-10-26 07:13:44 -10:00
Jonathan Swoboda 87341dc136 [logger] Enable Log V2 only on the native ESP-IDF toolchain (#21652) 2026-10-26 13:03:15 -04:00
J. Nick Koston cfe5809654 [wifi] Hand the DPP bootstrapping key to the supplicant DER encoded (#20882) 2026-10-26 16:37:25 +00:00
J. Nick Koston 9c54f96520 [libretiny] Send only the UF2 images the device writes over OTA (#21642) 2026-10-26 06:28:54 -10:00
J. Nick Kostonandpre-commit-ci-lite[bot] 0cc5b372fb [core] Defer integration imports in ir_rf_base and media_player (#21601)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-26 06:26:00 -10:00
J. Nick Koston f7c8cba8b7 [components] Use pulse_reset for blocking reset pin pulses (#21472) 2026-10-26 06:22:11 -10:00
J. Nick Koston 79a22e9a9a [core] Add pulse_reset for active-low reset pins (#21470) 2026-10-26 06:22:10 -10:00
J. Nick Koston 103023737d [openthread] Resolve IPv6 literal addresses when the DNS64 client is enabled (#19269) 2026-10-26 16:18:12 +00:00
J. Nick Koston 6b6b70a8b6 [dfrobot_sen0395] Collect lines with LineReader (#20979) 2026-10-26 12:16:51 -04:00
J. Nick Koston d312bf1b73 [core] Let the yaml editor insert lines, write with a rollback and restore (#20602) 2026-10-26 06:16:26 -10:00
J. Nick Koston 0d2f8c0e88 [components] Set or clear entity, socket and sensor bits with assign_bits (#21188) 2026-10-26 12:10:29 -04:00
J. Nick Koston 2f8b50c5af [components] Build the remaining enum option dicts with options() (#20972) 2026-10-26 12:10:08 -04:00
Kevin Ahrendt c4b598be3e [microphone] Pass each audio buffer as an AudioCapture (#21645) 2026-10-26 06:09:53 -10:00
J. Nick Koston 0cd16df16f [components] Write constant codec register tables with write_registers (#20946) 2026-10-26 12:09:47 -04:00
J. Nick Koston a680974da2 [ld6002b] Use Apply entities and find the setting by entity pointer (#20937) 2026-10-26 12:09:03 -04:00
J. Nick Koston 78f0b1ceba [esp32_rmt_led_strip] Stop blocking the main loop while waiting for the previous frame (#19337) 2026-10-26 06:08:13 -10:00
J. Nick Koston c5a841e11e [seeed_mr60fda2] Keep the parameter value tables in flash (#20720) 2026-10-26 12:07:24 -04:00
J. Nick Koston 365965f9bb [veml7700] Keep the gain and integration time tables in flash (#20719) 2026-10-26 12:07:06 -04:00
J. Nick Koston 16011d7fce [nextion] Keep the queue type name table in flash (#20658) 2026-10-26 12:06:31 -04:00
J. Nick Koston 4486dc53d8 [tx20] Keep the wind direction table in flash (#20657) 2026-10-26 12:05:43 -04:00
8d5e3ce534 [components] Use Apply selects for hub forwarding selects (#20966)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-26 16:05:20 +00:00
J. Nick Koston 9ea4d35a50 [uart] Keep debug delimiters and event patterns in flash (#20109) 2026-10-26 12:05:06 -04:00
J. Nick Koston 5cceddfbf4 [kamstrup_kmp] Keep the unit name table in flash (#20642) 2026-10-26 11:58:43 -04:00
J. Nick Koston ff6a37a052 [micronova] Keep the stove state table in flash (#20645) 2026-10-26 11:58:15 -04:00
J. Nick Koston 6990092be0 [ezo] Keep the command and calibration type tables in flash (#20655) 2026-10-26 11:57:41 -04:00
J. Nick Koston 9220c79a51 [bme680_bsec] Keep the IAQ accuracy names in flash (#20656) 2026-10-26 11:57:23 -04:00
J. Nick Koston 215fcd4f5d [pulse_counter] Keep the edge mode name table in flash (#20660) 2026-10-26 11:57:03 -04:00
J. Nick Koston 918e3b3a58 [haier] Keep the protocol phase names in flash (#20710) 2026-10-26 11:56:43 -04:00
J. Nick Koston 347eb3902e [seeed_mr24hpc1] Keep the text sensor state tables in flash (#20711) 2026-10-26 11:55:18 -04:00
J. Nick Koston 177820083f [epaper_spi] Keep the state name table in flash (#20713) 2026-10-26 11:54:07 -04:00
J. Nick Koston b0a0524091 [cc1101] Keep the modulation name table in flash (#20716) 2026-10-26 11:53:10 -04:00
250 changed files with 3663 additions and 4418 deletions

No files matched your search

+17 -10
View File
@@ -1305,10 +1305,7 @@ def upload_program(
f"{option_string} is only supported with the esphome OTA platform; "
"the web_server OTA path can only update the firmware image."
)
binary = CORE.firmware_bin
if getattr(args, "file", None) is not None:
binary = Path(args.file)
return _upload_via_web_server(config, network_devices, binary)
return _upload_via_web_server(config, network_devices, _app_ota_image(args))
return _upload_via_native_api(config, network_devices, args)
@@ -1364,6 +1361,16 @@ def _esphome_ota_conf(config: ConfigType) -> ConfigType | None:
return next(iter(esphome_ota_items(config)), None)
def _app_ota_image(args: ArgsProtocol) -> Path:
"""The file an app update sends: ``--file`` or the build output, as the
target platform's ota_image hook picks it."""
binary = CORE.firmware_bin
if (file := getattr(args, "file", None)) is not None:
binary = Path(file)
ota_image = platform_hooks.get_platform_hook(CORE.target_platform, "ota_image")
return ota_image(binary) if ota_image is not None else binary
def _upload_via_native_api(
config: ConfigType, network_devices: list[str], args: ArgsProtocol
) -> tuple[int, str | None]:
@@ -1413,27 +1420,27 @@ def _upload_via_native_api(
f"flash a build with the option enabled, and then retry {option_string}."
)
binary = CORE.firmware_bin
ota_type = espota2.OTA_TYPE_UPDATE_APP
file = getattr(args, "file", None)
if getattr(args, "partition_table", False):
# Fail fast if the resolved ESPHome OTA config does not enable allow_partition_access.
# The device-side handshake also rejects this with "Device only supports app updates",
# but checking here surfaces the misconfiguration before opening a network connection.
check_partition_access("--partition-table")
binary = CORE.partition_table_bin
binary = Path(file) if file is not None else CORE.partition_table_bin
ota_type = espota2.OTA_TYPE_UPDATE_PARTITION_TABLE
elif getattr(args, "bootloader", False):
check_partition_access("--bootloader")
if (
getattr(args, "file", None) is None
file is None
and (native := native_backend())
and (hint := getattr(native, "missing_image_hint", lambda: None)())
):
raise EsphomeError(hint)
binary = CORE.bootloader_bin
binary = Path(file) if file is not None else CORE.bootloader_bin
ota_type = espota2.OTA_TYPE_UPDATE_BOOTLOADER
if getattr(args, "file", None) is not None:
binary = Path(args.file)
else:
binary = _app_ota_image(args)
if ota_type == espota2.OTA_TYPE_UPDATE_PARTITION_TABLE:
_validate_partition_table_binary(binary)
+27 -1
View File
@@ -33,9 +33,35 @@ _CACHE_VERSION = 1
_LAMBDA_KEY = "__esphome_lambda__"
_CACHE_SUFFIX = ".validated.json"
def compiled_config_path(config_filename: str) -> Path:
"""Path to the cached validated config alongside the storage sidecar."""
return CORE.data_dir / "storage" / f"{config_filename}.validated.json"
return CORE.data_dir / "storage" / f"{config_filename}{_CACHE_SUFFIX}"
def invalidate_compiled_config() -> None:
"""Drop every cache in the storage dir after a change to a file the
mtime check cannot see, such as secrets.yaml or an include; other
configurations may share that file."""
storage = compiled_config_path(CORE.config_filename).parent
try:
caches = [p for p in storage.iterdir() if p.name.endswith(_CACHE_SUFFIX)]
except FileNotFoundError:
return
except OSError as err:
raise EsphomeError(
f"Could not list the validated config caches in {storage}: {err}"
) from err
for path in caches:
try:
path.unlink(missing_ok=True)
except OSError as err:
# A stale cache would present the old key on the next upload
raise EsphomeError(
f"Could not remove the validated config cache {path}: {err}"
) from err
def save_compiled_config(config: ConfigType) -> None:
+1 -3
View File
@@ -297,9 +297,7 @@ void ADE7880::init_device_() {
void ADE7880::reset_device_() {
if (this->reset_pin_ != nullptr) {
ESP_LOGD(TAG, "Reset device using RESET pin");
this->reset_pin_->digital_write(false);
delay(1);
this->reset_pin_->digital_write(true);
pulse_reset(this->reset_pin_, {.low_ms = 1});
} else {
ESP_LOGD(TAG, "Reset device using SWRST command");
this->write_u16_register16_(CONFIG, CONFIG_SWRST);
+73 -73
View File
@@ -14,80 +14,80 @@ ESPHOME_LOG_TAG(TAG, "aic3204");
return; \
}
static constexpr i2c::I2CRegValue AIC3204_INIT[] PROGMEM = {
// Set register page to 0
{AIC3204_PAGE_CTRL, 0x00},
// Initiate SW reset (PLL is powered off as part of reset)
{AIC3204_SW_RST, 0x01},
// *** Program clock settings ***
// Default is CODEC_CLKIN is from MCLK pin. Don't need to change this.
// MDAC*NDAC*FOSR*48Khz = mClk (24.576 MHz when the XMOS is expecting 48kHz audio)
// (See page 51 of https://www.ti.com/lit/ml/slaa557/slaa557.pdf)
// We do need MDAC*DOSR/32 >= the resource compute level for the processing block
// So here 2*128/32 = 8, which is equal to processing block 1 's resource compute
// See page 5 of https://www.ti.com/lit/an/slaa404c/slaa404c.pdf for the workflow
// for determining these settings.
// Power up NDAC and set to 2
{AIC3204_NDAC, 0x82},
// Power up MDAC and set to 2
{AIC3204_MDAC, 0x82},
// Program DOSR = 128
{AIC3204_DOSR, 0x80},
// Set Audio Interface Config: I2S, 32 bits, DOUT always driving
{AIC3204_CODEC_IF, 0x30},
// For I2S Firmware only, set SCLK/MFP3 pin as Audio Data In
{AIC3204_SCLK_MFP3, 0x02},
{AIC3204_AUDIO_IF_4, 0x01},
{AIC3204_AUDIO_IF_5, 0x01},
// Program the DAC processing block to be used - PRB_P1
{AIC3204_DAC_SIG_PROC, 0x01},
// *** Select Page 1 ***
{AIC3204_PAGE_CTRL, 0x01},
// Enable the internal AVDD_LDO:
{AIC3204_LDO_CTRL, 0x09},
// *** Program Analog Blocks ***
// Disable Internal Crude AVdd in presence of external AVdd supply or before powering up internal AVdd LDO
{AIC3204_PWR_CFG, 0x08},
// Enable Master Analog Power Control
{AIC3204_LDO_CTRL, 0x01},
// Page 125: Common mode control register, set d6 to 1 to make the full chip common mode = 0.75 v
// We are using the internal AVdd regulator with a nominal output of 1.72 V (see LDO_CTRL_REGISTER on page 123)
// Page 86 says to only set the common mode voltage to 0.9 v if AVdd >= 1.8... but it isn't on our hardware
// We also adjust the HPL and HPR gains to -2dB gian later in this config flow compensate (see page 47)
// (All pages refer to the TLV320AIC3204 Application Reference Guide)
{AIC3204_CM_CTRL, 0x40},
// *** Set PowerTune Modes ***
// Set the Left & Right DAC PowerTune mode to PTM_P3/4. Use Class-AB driver.
{AIC3204_PLAY_CFG1, 0x00},
{AIC3204_PLAY_CFG2, 0x00},
// Set the REF charging time to 40ms
{AIC3204_REF_STARTUP, 0x01},
// HP soft stepping settings for optimal pop performance at power up
// Rpop used is 6k with N = 6 and soft step = 20usec. This should work with 47uF coupling
// capacitor. Can try N=5,6 or 7 time constants as well. Trade-off delay vs “pop” sound.
{AIC3204_HP_START, 0x25},
// Route Left DAC to HPL
{AIC3204_HPL_ROUTE, 0x08},
// Route Right DAC to HPR
{AIC3204_HPR_ROUTE, 0x08},
// Route Left DAC to LOL
{AIC3204_LOL_ROUTE, 0x08},
// Route Right DAC to LOR
{AIC3204_LOR_ROUTE, 0x08},
// Unmute HPL and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
{AIC3204_HPL_GAIN, 0x3e},
// Unmute HPR and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
{AIC3204_HPR_GAIN, 0x3e},
// Unmute LOL and set gain to 0dB
{AIC3204_LOL_DRV_GAIN, 0x00},
// Unmute LOR and set gain to 0dB
{AIC3204_LOR_DRV_GAIN, 0x00},
// Power up HPL and HPR, LOL and LOR drivers
{AIC3204_OP_PWR_CTRL, 0x3C},
};
void AIC3204::setup() {
// Set register page to 0
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed");
// Initiate SW reset (PLL is powered off as part of reset)
ERROR_CHECK(this->write_byte(AIC3204_SW_RST, 0x01), "Software reset failed");
// *** Program clock settings ***
// Default is CODEC_CLKIN is from MCLK pin. Don't need to change this.
// MDAC*NDAC*FOSR*48Khz = mClk (24.576 MHz when the XMOS is expecting 48kHz audio)
// (See page 51 of https://www.ti.com/lit/ml/slaa557/slaa557.pdf)
// We do need MDAC*DOSR/32 >= the resource compute level for the processing block
// So here 2*128/32 = 8, which is equal to processing block 1 's resource compute
// See page 5 of https://www.ti.com/lit/an/slaa404c/slaa404c.pdf for the workflow
// for determining these settings.
// Power up NDAC and set to 2
ERROR_CHECK(this->write_byte(AIC3204_NDAC, 0x82), "Set NDAC failed");
// Power up MDAC and set to 2
ERROR_CHECK(this->write_byte(AIC3204_MDAC, 0x82), "Set MDAC failed");
// Program DOSR = 128
ERROR_CHECK(this->write_byte(AIC3204_DOSR, 0x80), "Set DOSR failed");
// Set Audio Interface Config: I2S, 32 bits, DOUT always driving
ERROR_CHECK(this->write_byte(AIC3204_CODEC_IF, 0x30), "Set CODEC_IF failed");
// For I2S Firmware only, set SCLK/MFP3 pin as Audio Data In
ERROR_CHECK(this->write_byte(AIC3204_SCLK_MFP3, 0x02), "Set SCLK/MFP3 failed");
ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_4, 0x01), "Set AUDIO_IF_4 failed");
ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_5, 0x01), "Set AUDIO_IF_5 failed");
// Program the DAC processing block to be used - PRB_P1
ERROR_CHECK(this->write_byte(AIC3204_DAC_SIG_PROC, 0x01), "Set DAC_SIG_PROC failed");
// *** Select Page 1 ***
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x01), "Set page 1 failed");
// Enable the internal AVDD_LDO:
ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x09), "Set LDO_CTRL failed");
// *** Program Analog Blocks ***
// Disable Internal Crude AVdd in presence of external AVdd supply or before powering up internal AVdd LDO
ERROR_CHECK(this->write_byte(AIC3204_PWR_CFG, 0x08), "Set PWR_CFG failed");
// Enable Master Analog Power Control
ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x01), "Set LDO_CTRL failed");
// Page 125: Common mode control register, set d6 to 1 to make the full chip common mode = 0.75 v
// We are using the internal AVdd regulator with a nominal output of 1.72 V (see LDO_CTRL_REGISTER on page 123)
// Page 86 says to only set the common mode voltage to 0.9 v if AVdd >= 1.8... but it isn't on our hardware
// We also adjust the HPL and HPR gains to -2dB gian later in this config flow compensate (see page 47)
// (All pages refer to the TLV320AIC3204 Application Reference Guide)
ERROR_CHECK(this->write_byte(AIC3204_CM_CTRL, 0x40), "Set CM_CTRL failed");
// *** Set PowerTune Modes ***
// Set the Left & Right DAC PowerTune mode to PTM_P3/4. Use Class-AB driver.
ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG1, 0x00), "Set PLAY_CFG1 failed");
ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG2, 0x00), "Set PLAY_CFG2 failed");
// Set the REF charging time to 40ms
ERROR_CHECK(this->write_byte(AIC3204_REF_STARTUP, 0x01), "Set REF_STARTUP failed");
// HP soft stepping settings for optimal pop performance at power up
// Rpop used is 6k with N = 6 and soft step = 20usec. This should work with 47uF coupling
// capacitor. Can try N=5,6 or 7 time constants as well. Trade-off delay vs “pop” sound.
ERROR_CHECK(this->write_byte(AIC3204_HP_START, 0x25), "Set HP_START failed");
// Route Left DAC to HPL
ERROR_CHECK(this->write_byte(AIC3204_HPL_ROUTE, 0x08), "Set HPL_ROUTE failed");
// Route Right DAC to HPR
ERROR_CHECK(this->write_byte(AIC3204_HPR_ROUTE, 0x08), "Set HPR_ROUTE failed");
// Route Left DAC to LOL
ERROR_CHECK(this->write_byte(AIC3204_LOL_ROUTE, 0x08), "Set LOL_ROUTE failed");
// Route Right DAC to LOR
ERROR_CHECK(this->write_byte(AIC3204_LOR_ROUTE, 0x08), "Set LOR_ROUTE failed");
// Unmute HPL and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
ERROR_CHECK(this->write_byte(AIC3204_HPL_GAIN, 0x3e), "Set HPL_GAIN failed");
// Unmute HPR and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
ERROR_CHECK(this->write_byte(AIC3204_HPR_GAIN, 0x3e), "Set HPR_GAIN failed");
// Unmute LOL and set gain to 0dB
ERROR_CHECK(this->write_byte(AIC3204_LOL_DRV_GAIN, 0x00), "Set LOL_DRV_GAIN failed");
// Unmute LOR and set gain to 0dB
ERROR_CHECK(this->write_byte(AIC3204_LOR_DRV_GAIN, 0x00), "Set LOR_DRV_GAIN failed");
// Power up HPL and HPR, LOL and LOR drivers
ERROR_CHECK(this->write_byte(AIC3204_OP_PWR_CTRL, 0x3C), "Set OP_PWR_CTRL failed");
ERROR_CHECK(this->write_registers(AIC3204_INIT), "Failed to initialize");
// Wait for 2.5 sec for soft stepping to take effect before attempting power-up
this->set_timeout(2500, [this]() {
+4 -8
View File
@@ -174,14 +174,10 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType:
CONF_SUPPORTS_RESPONSE = "supports_response"
# Enum values in api::enums namespace
enums_ns = api_ns.namespace("enums")
SUPPORTS_RESPONSE_OPTIONS = {
"none": enums_ns.SUPPORTS_RESPONSE_NONE,
"optional": enums_ns.SUPPORTS_RESPONSE_OPTIONAL,
"only": enums_ns.SUPPORTS_RESPONSE_ONLY,
"status": enums_ns.SUPPORTS_RESPONSE_STATUS,
}
SupportsResponseType = api_ns.namespace("enums").enum("SupportsResponseType")
SUPPORTS_RESPONSE_OPTIONS = SupportsResponseType.options(
"none", "optional", "only", "status", prefix="SUPPORTS_RESPONSE_"
)
def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
+7 -6
View File
@@ -54,7 +54,8 @@ static const char AS7341_NAME[] PROGMEM = "AS7341";
static constexpr uint8_t AS7341_ADC_SLOTS = 6;
static constexpr uint8_t AS7341_SMUX_LEN = 20;
static constexpr uint8_t AS7341_SMUX_STEPS = 2;
static const RegValue AS7341_INIT[] PROGMEM = {{0x70, 0x00}}; // CONFIG: SPM mode, INT_SEL and LED_SEL cleared
static constexpr i2c::I2CRegValue AS7341_INIT[] PROGMEM = {
{0x70, 0x00}}; // CONFIG: SPM mode, INT_SEL and LED_SEL cleared
static const uint8_t AS7341_SMUX[] PROGMEM = {
0x30, 0x01, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x50, 0x00, // step 0: F1 to F4, NIR, Clear
0x00, 0x00, 0x20, 0x04, 0x00, 0x30, 0x01, 0x50, 0x00, 0x06, // step 0, continued
@@ -150,8 +151,8 @@ static constexpr uint8_t AS7343_ADC_SLOTS = 18;
static constexpr uint8_t AS7343_SMUX_LEN = 10;
static const char TCS3448_NAME[] PROGMEM = "TCS3448";
// CFG6, FD_CFG0, CFG10, CFG8, CFG20 (auto_smux, 18 channels in 3 cycles), AGC_GAIN_MAX, FD_TIME_1, FD_TIME_2
static const RegValue AS7343_INIT[] PROGMEM = {{0xF5, 0x00}, {0xDF, 0xA1}, {0x65, 0xF2}, {0xC9, 0xC8},
{0xD6, 0x62}, {0xD7, 0x99}, {0xE0, 0x64}, {0xE2, 0x21}};
static constexpr i2c::I2CRegValue AS7343_INIT[] PROGMEM = {{0xF5, 0x00}, {0xDF, 0xA1}, {0x65, 0xF2}, {0xC9, 0xC8},
{0xD6, 0x62}, {0xD7, 0x99}, {0xE0, 0x64}, {0xE2, 0x21}};
static const uint8_t AS7343_SMUX[] PROGMEM = {0x00, 0x04, 0x65, 0x02, 0x00, 0x05, 0x00, 0x01, 0x00, 0x30,
0x00, 0x00, 0x60, 0x20, 0x04, 0x50, 0x03, 0x00, 0x01, 0x05,
0x05, 0x00, 0x60, 0x00, 0x30, 0x00, 0x40, 0x10, 0x20, 0x00};
@@ -230,7 +231,7 @@ static constexpr ChipInfo as7343_chip(const char *name) {
.gain_correction = AS7343_GAIN_CORRECTION,
.contributions = AS7343_CONTRIBUTIONS,
.tristimulus = AS7343_TRISTIMULUS,
.init_count = sizeof(AS7343_INIT) / sizeof(RegValue),
.init_count = sizeof(AS7343_INIT) / sizeof(i2c::I2CRegValue),
.astep_reg = 0xD4,
.cfg0_reg = 0xBF,
.cfg1_reg = 0xC6,
@@ -298,9 +299,9 @@ void AS734XComponent::setup() {
this->mark_failed(LOG_STR("Wrong chip ID"));
return;
}
const RegValue *init = pgm(chip->init);
const i2c::I2CRegValue *init = pgm(chip->init);
for (uint8_t i = 0, n = pgm(chip->init_count); ok && i < n; i++) {
RegValue rv;
i2c::I2CRegValue rv;
progmem_memcpy(&rv, &init[i], sizeof(rv));
ok = this->select_bank_(rv.reg) && this->write_byte(rv.reg, rv.value);
}
+1 -6
View File
@@ -68,15 +68,10 @@ struct ChannelTristimulus {
float z;
};
struct RegValue {
uint8_t reg;
uint8_t value;
};
/// Per-chip constants, stored in flash. The pointed-to tables are in flash too.
struct ChipInfo {
const char *name;
const RegValue *init; // written in order; the bank is selected per register address
const i2c::I2CRegValue *init; // written in order; the bank is selected per register address
const uint8_t *smux; // AS7341: SMUX RAM image per step; AS7343: the three chain images
const uint8_t *readout; // per step, the ADC slot of each band (0xFF: not read in this step)
const uint16_t *wavelengths; // per band, WIDEBAND_NM for clear and ABSENT_NM for a missing band
@@ -16,13 +16,7 @@ constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a,
}
void AXS15231Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
delay(10);
}
pulse_reset(this->reset_pin_, {.low_ms = 5, .high_after_ms = 10});
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
this->interrupt_pin_->setup();
+4
View File
@@ -78,3 +78,7 @@ async def pin_to_code(config: ConfigType) -> MockObj:
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
# Called by the OTA upload; the shared libretiny implementation.
ota_image = libretiny.device_ota_image
+116 -316
View File
@@ -1,23 +1,11 @@
from collections.abc import Callable, Coroutine
import functools
from collections.abc import Coroutine
from typing import Any
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import ble_device_base, bluetooth_connection
from esphome.components.ble_device_base import (
BT_UUID16_FORMAT as bt_uuid16_format,
BT_UUID32_FORMAT as bt_uuid32_format,
BT_UUID128_FORMAT as bt_uuid128_format,
as_hex,
as_reversed_hex_array,
bt_uuid,
)
from esphome.config_helpers import (
filter_source_files_from_platform,
frameworks_for_platforms,
)
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
from esphome.components.esp32_ble import BTLoggers
import esphome.config_validation as cv
from esphome.const import (
CONF_CHARACTERISTIC_UUID,
@@ -30,55 +18,15 @@ from esphome.const import (
CONF_SERVICE_UUID,
CONF_TRIGGER_ID,
CONF_VALUE,
PLATFORM_ESP32,
PlatformFramework,
)
from esphome.core import CORE, ID
from esphome.enum import StrEnum
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.core import ID
from esphome.types import ConfigType
# The esp32 BLE stack (esp32_ble, esp32_ble_tracker) is imported lazily inside
# the esp32 schema/codegen arms: importing those modules registers esp32-only
# automations as a side effect, which must not leak into the neutral
# platforms' registries (the bluetooth_proxy pattern).
def _legacy_engine() -> bool:
"""True when the build uses the legacy raw-gattc engine - one line to
flip when esp32 moves to the neutral engine (with
USE_BLE_CLIENT_LEGACY_ENGINE in _to_code_esp32)."""
return CORE.is_esp32
def AUTO_LOAD() -> list[str]:
"""The engine's closure per platform: the legacy esp32 engine builds on
esp32_ble_client plus bluetooth_connection (the shared service-table
materializer; its sources compile empty in builds without a neutral
node), the neutral engine on the bluetooth_connection backend. The
platform-less arm is the union for manifest-resolving tooling."""
if _legacy_engine() or CORE.target_platform is None:
return ["bluetooth_connection", "esp32_ble_client"]
return ["bluetooth_connection"]
AUTO_LOAD = ["esp32_ble_client"]
CODEOWNERS = ["@buxtronix", "@clydebarrow"]
DEPENDENCIES = ["esp32_ble_tracker"]
DOMAIN = "ble_client"
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"ble_client.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
# Every framework of every non-esp32 registry platform: a platform
# that validates the neutral arm must also compile the neutral engine.
"ble_client_gatt.cpp": frameworks_for_platforms(
set(bluetooth_connection.GATT_CLIENT_PLATFORMS) - {PLATFORM_ESP32}
),
}
)
CONF_DESCRIPTOR_UUID = "descriptor_uuid"
CONF_ON_NOTIFY = "on_notify"
@@ -114,12 +62,16 @@ def notify_from_on_notify(config: ConfigType) -> ConfigType:
ble_client_ns = cg.esphome_ns.namespace("ble_client")
# One codegen class for both engines: the exclusively-gated headers resolve
# the name to exactly one C++ definition per build.
BLEClient = ble_client_ns.class_("BLEClient", cg.Component)
BLEClient = ble_client_ns.class_("BLEClient", esp32_ble_client.BLEClientBase)
BLEClientNode = ble_client_ns.class_("BLEClientNode")
BLEClientNodeConstRef = BLEClientNode.operator("ref").operator("const")
# Triggers
BLEClientConnectTrigger = ble_client_ns.class_(
"BLEClientConnectTrigger", automation.Trigger.template(BLEClientNodeConstRef)
)
BLEClientDisconnectTrigger = ble_client_ns.class_(
"BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef)
)
BLEClientPasskeyRequestTrigger = ble_client_ns.class_(
"BLEClientPasskeyRequestTrigger", automation.Trigger.template(BLEClientNodeConstRef)
)
@@ -157,175 +109,62 @@ CONF_AUTO_CONNECT = "auto_connect"
MULTI_CONF = True
# Keys shared by both engines' schemas.
_COMMON_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLEClient),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_AUTO_CONNECT, default=True): cv.boolean,
cv.Optional(CONF_ON_CONNECT): automation.validate_automation({}),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation({}),
}
).extend(cv.COMPONENT_SCHEMA)
# Both engines register these on the client; the Bluedroid-security triggers below stay trigger classes
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(CONF_ON_CONNECT, "add_on_connect_callback"),
automation.CallbackAutomation(CONF_ON_DISCONNECT, "add_on_disconnect_callback"),
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLEClient),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_NAME): cv.string,
cv.Optional(CONF_AUTO_CONNECT, default=True): cv.boolean,
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
BLEClientConnectTrigger
),
}
),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
BLEClientDisconnectTrigger
),
}
),
cv.Optional(CONF_ON_PASSKEY_REQUEST): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
BLEClientPasskeyRequestTrigger
),
}
),
cv.Optional(CONF_ON_PASSKEY_NOTIFICATION): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
BLEClientPasskeyNotificationTrigger
),
}
),
cv.Optional(
CONF_ON_NUMERIC_COMPARISON_REQUEST
): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
BLEClientNumericComparisonRequestTrigger
),
}
),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA),
esp32_ble.consume_connection_slots(1, "ble_client"),
)
@functools.cache
def _esp32_config_schema() -> cv.All:
"""The legacy engine's schema, byte-compatible with what esp32 always had
(including the Bluedroid security triggers)."""
from esphome.components import esp32_ble_tracker
return cv.All(
_COMMON_SCHEMA.extend(
{
# Accepted-but-unused legacy key; not propagated to the
# neutral schema.
cv.Optional(CONF_NAME): cv.string,
cv.Optional(CONF_ON_PASSKEY_REQUEST): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
BLEClientPasskeyRequestTrigger
),
}
),
cv.Optional(
CONF_ON_PASSKEY_NOTIFICATION
): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
BLEClientPasskeyNotificationTrigger
),
}
),
cv.Optional(
CONF_ON_NUMERIC_COMPARISON_REQUEST
): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
BLEClientNumericComparisonRequestTrigger
),
}
),
}
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA),
bluetooth_connection.consume_gatt_slot("ble_client"),
)
@functools.cache
def _gatt_config_schema(platform: str) -> cv.All:
"""The neutral engine's schema: the shared keys plus the hub reference
(parsed-advertisement sightings) and the GATT backend declaration.
Keyed by platform - the backend fragment differs per platform."""
return cv.All(
_COMMON_SCHEMA.extend(ble_device_base.BLE_DEVICE_SCHEMA).extend(
bluetooth_connection.gatt_client_schema(platform)
),
bluetooth_connection.consume_gatt_slot("ble_client"),
)
@schema_extractor("schema")
def _validate_platform(config: ConfigType) -> ConfigType:
if config is SCHEMA_EXTRACT:
# Deliberate gap (the bluetooth_proxy pattern): the dumper gets only
# this shape, so the neutral arm's ble_hub_id is absent from editor
# schemas and the esp32-only keys are advertised on every platform.
# The language-schema dumper runs without a platform; expose the
# esp32 (legacy-engine) shape.
return _esp32_config_schema()
if _legacy_engine():
return _esp32_config_schema()(config)
if CORE.target_platform in bluetooth_connection.GATT_CLIENT_PLATFORMS:
return _gatt_config_schema(CORE.target_platform)(config)
raise cv.Invalid(f"ble_client is not supported on {CORE.target_platform}")
CONFIG_SCHEMA = _validate_platform
CONF_BLE_CLIENT_ID = "ble_client_id"
class BLEClientFeatures(StrEnum):
"""Per-platform engine capabilities consumers declare against."""
# The platform-neutral node interface (on_connected/table + completion
# callbacks) - every platform with a ble_client engine.
GATT_NODE = "gatt_node"
# The raw esp32 GATT client event stream (gattc/gap handlers,
# node_state) - the legacy engine only.
RAW_GATTC = "raw_gattc"
# Pairing dialog replies and bond management (Bluedroid GAP/SMP).
SECURITY = "security"
def _engine_features() -> set[BLEClientFeatures]:
"""Features the validated platform's engine provides."""
if _legacy_engine():
return {
BLEClientFeatures.GATT_NODE,
BLEClientFeatures.RAW_GATTC,
BLEClientFeatures.SECURITY,
}
if CORE.target_platform in bluetooth_connection.GATT_CLIENT_PLATFORMS:
return {BLEClientFeatures.GATT_NODE}
return set()
def requires_feature(
feature: BLEClientFeatures, description: str
) -> Callable[[Any], Any]:
"""Validator gating a consumer to platforms whose engine provides
`feature`, naming the missing capability in the error."""
def validator(value: Any) -> Any:
features = _engine_features()
if feature not in features:
available = (
f"; this platform's engine provides: {', '.join(sorted(features))}"
if features
else ""
)
raise cv.Invalid(
f"{description} requires the ble_client '{feature}' feature, "
f"which {CORE.target_platform} does not provide{available}"
)
return value
return validator
# The one choke point for every node component still on the raw esp32 event
# stream; migrating to the neutral interface (NODE_BLE_CLIENT_SCHEMA +
# register_gatt_node) lifts it.
_legacy_engine_only = requires_feature(
BLEClientFeatures.RAW_GATTC,
"This component drives the raw ESP32 GATT client events and has not "
"been migrated to the platform-neutral node interface yet; it",
)
BLE_CLIENT_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_BLE_CLIENT_ID): cv.All(
cv.use_id(BLEClient), _legacy_engine_only
),
}
)
# For node components on the neutral interface: valid wherever ble_client
# itself is.
NODE_BLE_CLIENT_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_BLE_CLIENT_ID): cv.All(
cv.use_id(BLEClient),
requires_feature(BLEClientFeatures.GATT_NODE, "This component"),
),
cv.GenerateID(CONF_BLE_CLIENT_ID): cv.use_id(BLEClient),
}
)
@@ -335,26 +174,6 @@ async def register_ble_node(var, config):
cg.add(parent.register_ble_node(var))
def _request_gatt_node_build() -> None:
"""Node storage and the one define meaning "the neutral node surface is
compiled in", plus the esp32 bridge/materializer defines."""
_request_node_slot()
cg.add_define("USE_BLE_CLIENT_GATT_NODES")
if _legacy_engine():
# Deliberately not ble_device_base.request_gatt_client(): that would
# claim a phantom backend slot on combined proxy builds.
cg.add_define("USE_BLE_GATT_CLIENT")
cg.add_define("USE_BLE_GATT_BACKEND_BLUEDROID")
cg.add_define("USE_BLUEDROID_GATT_SERVICE_TABLE")
async def register_gatt_node(var, config):
"""Register a node on the platform-neutral interface (both engines)."""
parent = await cg.get_variable(config[CONF_BLE_CLIENT_ID])
_request_gatt_node_build()
cg.add(parent.register_gatt_node(var))
def new_ble_node_component(
config: ConfigType, *args: Any
) -> Coroutine[Any, Any, cg.MockObj]:
@@ -365,8 +184,8 @@ def new_ble_node_component(
BLE_WRITE_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
cv.Required(CONF_SERVICE_UUID): bt_uuid,
cv.Required(CONF_CHARACTERISTIC_UUID): bt_uuid,
cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid,
cv.Required(CONF_VALUE): cv.templatable(cv.ensure_list(cv.hex_uint8_t)),
}
)
@@ -377,34 +196,25 @@ BLE_CONNECT_ACTION_SCHEMA = maybe_simple_id(
}
)
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.All(
requires_feature(BLEClientFeatures.SECURITY, "This action"),
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
cv.Required(CONF_ACCEPT): cv.templatable(cv.boolean),
}
),
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
cv.Required(CONF_ACCEPT): cv.templatable(cv.boolean),
}
)
BLE_PASSKEY_REPLY_ACTION_SCHEMA = cv.All(
requires_feature(BLEClientFeatures.SECURITY, "This action"),
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
cv.Required(CONF_PASSKEY): cv.templatable(cv.int_range(min=0, max=999999)),
}
),
BLE_PASSKEY_REPLY_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
cv.Required(CONF_PASSKEY): cv.templatable(cv.int_range(min=0, max=999999)),
}
)
BLE_REMOVE_BOND_ACTION_SCHEMA = cv.All(
requires_feature(BLEClientFeatures.SECURITY, "This action"),
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
}
),
BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
}
)
@@ -432,8 +242,6 @@ automation.register_simple_action(
)
async def ble_write_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
# The action registers itself as a neutral node in its constructor.
_request_gatt_node_build()
var = cg.new_Pvariable(action_id, template_arg, parent)
value = config[CONF_VALUE]
@@ -448,20 +256,38 @@ async def ble_write_to_code(config, action_id, template_arg, args):
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*value))
cg.add(var.set_value_simple(arr, len(value)))
if len(config[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
cg.add(var.set_service_uuid16(as_hex(config[CONF_SERVICE_UUID])))
elif len(config[CONF_SERVICE_UUID]) == len(bt_uuid32_format):
cg.add(var.set_service_uuid32(as_hex(config[CONF_SERVICE_UUID])))
elif len(config[CONF_SERVICE_UUID]) == len(bt_uuid128_format):
uuid128 = as_reversed_hex_array(config[CONF_SERVICE_UUID])
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
cg.add(
var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))
)
elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format):
cg.add(
var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))
)
elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format):
uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID])
cg.add(var.set_service_uuid128(uuid128))
if len(config[CONF_CHARACTERISTIC_UUID]) == len(bt_uuid16_format):
cg.add(var.set_char_uuid16(as_hex(config[CONF_CHARACTERISTIC_UUID])))
elif len(config[CONF_CHARACTERISTIC_UUID]) == len(bt_uuid32_format):
cg.add(var.set_char_uuid32(as_hex(config[CONF_CHARACTERISTIC_UUID])))
elif len(config[CONF_CHARACTERISTIC_UUID]) == len(bt_uuid128_format):
uuid128 = as_reversed_hex_array(config[CONF_CHARACTERISTIC_UUID])
if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
cg.add(
var.set_char_uuid16(
esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID])
)
)
elif len(config[CONF_CHARACTERISTIC_UUID]) == len(
esp32_ble_tracker.bt_uuid32_format
):
cg.add(
var.set_char_uuid32(
esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID])
)
)
elif len(config[CONF_CHARACTERISTIC_UUID]) == len(
esp32_ble_tracker.bt_uuid128_format
):
uuid128 = esp32_ble_tracker.as_reversed_hex_array(
config[CONF_CHARACTERISTIC_UUID]
)
cg.add(var.set_char_uuid128(uuid128))
return var
@@ -515,48 +341,22 @@ automation.register_simple_action(
)
async def _to_code_esp32(config: ConfigType) -> cg.MockObj:
from esphome.components import esp32_ble, esp32_ble_tracker
from esphome.components.esp32_ble import BTLoggers
async def to_code(config):
# Register the loggers this component needs
esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP)
cg.add_define("USE_ESP32_BLE_UUID")
cg.add_define("USE_BLE_CLIENT_LEGACY_ENGINE")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await esp32_ble_tracker.register_client(var, config)
return var
# Sizes the neutral client's node storage; the client itself requests a
# baseline slot so the define exists on every build that compiles the engine.
_request_node_slot = cg.slot_counter("ESPHOME_BLE_CLIENT_MAX_NODES")
async def _to_code_gatt(config: ConfigType) -> cg.MockObj:
# The engine always carries the node surface (the client itself owns the
# baseline slot).
_request_gatt_node_build()
backend = await bluetooth_connection.new_gatt_backend(config)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_backend(backend))
# Sighting-gated connects: the client listens for the peer's parsed
# advertisements through the hub.
await ble_device_base.register_ble_device(var, config)
return var
async def to_code(config: ConfigType) -> None:
if _legacy_engine():
var = await _to_code_esp32(config)
else:
var = await _to_code_gatt(config)
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
cg.add(var.set_auto_connect(config[CONF_AUTO_CONNECT]))
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
for conf in config.get(CONF_ON_CONNECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_DISCONNECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_PASSKEY_REQUEST, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
@@ -0,0 +1,11 @@
#ifdef USE_ESP32
#include "automation.h"
namespace esphome::ble_client {
const char *const Automation::TAG = "ble_client.automation"; // NOLINT: ble_client never builds for ESP8266
} // namespace esphome::ble_client
#endif
+192 -2
View File
@@ -1,14 +1,27 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#ifdef USE_BLE_CLIENT_LEGACY_ENGINE
#include <utility>
#include <vector>
#include "esphome/core/automation.h"
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
// Maximum bytes to log in hex format for BLE writes (many logging buffers are 256 chars)
static constexpr size_t BLE_WRITE_MAX_LOG_BYTES = 64;
namespace esphome::ble_client {
// placeholder class for static TAG .
class Automation {
public:
// could be made inline with C++17
static const char *const TAG;
};
// Base for nodes that never read the parent's services.
// The parent releases its services only once every node reports Established, so a node that never
// reports it keeps that memory allocated for the life of the connection.
@@ -26,6 +39,45 @@ class BLEClientServicelessNode : public BLEClientNode {
virtual void on_gattc_event(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {}
};
// implement on_connect automation.
class BLEClientConnectTrigger final : public Trigger<>, public BLEClientNode {
public:
explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); }
void loop() override {}
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override {
if (event == ESP_GATTC_SEARCH_CMPL_EVT) {
this->node_state = espbt::ClientState::ESTABLISHED;
this->trigger();
}
}
};
// on_disconnect automation
class BLEClientDisconnectTrigger final : public Trigger<>, public BLEClientNode {
public:
explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); }
void loop() override {}
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override {
// test for CLOSE and not DISCONNECT - DISCONNECT can occur even if no virtual connection (OPEN event) occurred.
// So this will not trigger unless a complete open has previously succeeded.
switch (event) {
case ESP_GATTC_SEARCH_CMPL_EVT: {
this->node_state = espbt::ClientState::ESTABLISHED;
break;
}
case ESP_GATTC_CLOSE_EVT: {
this->trigger();
break;
}
default: {
break;
}
}
}
};
class BLEClientPasskeyRequestTrigger final : public Trigger<>, public BLEClientServicelessNode {
public:
explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); }
@@ -58,6 +110,144 @@ class BLEClientNumericComparisonRequestTrigger final : public Trigger<uint32_t>,
}
};
// implement the ble_client.ble_write action.
template<typename... Ts> class BLEClientWriteAction final : public Action<Ts...>, public BLEClientNode {
public:
BLEClientWriteAction(BLEClient *ble_client) {
ble_client->register_ble_node(this);
ble_client_ = ble_client;
}
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
this->value_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
}
// Store pointer to static data in flash (no RAM copy)
void set_value_simple(const uint8_t *data, size_t len) {
this->value_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
}
void play(const Ts &...x) override {}
void play_complex(const Ts &...x) override {
this->num_running_++;
this->var_ = std::make_tuple(x...);
bool result;
if (this->len_ >= 0) {
// Static mode: write directly from flash pointer
result = this->write(this->value_.data, this->len_);
} else {
// Template mode: call function and write the vector
std::vector<uint8_t> value = this->value_.func(x...);
result = this->write(value);
}
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
if (!result)
this->play_next_(x...);
}
/**
* Note about logging: the esph_log_X macros are used here because the CI checks complain about use of the ESP LOG
* macros in header files (Can't even write it in a comment!)
* Not sure why, because they seem to work just fine.
* The problem is that the implementation of a templated class can't be placed in a .cpp file when using C++ less than
* 17, so the methods have to be here. The esph_log_X macros are equivalent in function, but don't trigger the CI
* errors.
*/
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
bool write(const uint8_t *data, size_t len) {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
return false;
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(BLE_WRITE_MAX_LOG_BYTES)];
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len));
#endif
esp_err_t err =
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len,
const_cast<uint8_t *>(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE);
if (err != ESP_OK) {
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
return false;
}
return true;
}
bool write(const std::vector<uint8_t> &value) { return this->write(value.data(), value.size()); }
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override {
switch (event) {
case ESP_GATTC_WRITE_CHAR_EVT:
// upstream code checked the MAC address, verify the characteristic.
if (param->write.handle == this->char_handle_)
this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); });
break;
case ESP_GATTC_DISCONNECT_EVT:
if (this->num_running_ != 0)
this->stop_complex();
break;
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
if (chr == nullptr) {
char char_buf[esp32_ble::UUID_STR_LEN];
char service_buf[esp32_ble::UUID_STR_LEN];
esph_log_w("ble_write_action", "Characteristic %s was not found in service %s",
this->char_uuid_.to_str(char_buf), this->service_uuid_.to_str(service_buf));
break;
}
this->char_handle_ = chr->handle;
this->char_props_ = chr->properties;
if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) {
this->write_type_ = ESP_GATT_WRITE_TYPE_RSP;
esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP");
} else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) {
this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP;
esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
} else {
char char_buf[esp32_ble::UUID_STR_LEN];
esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_str(char_buf));
break;
}
this->node_state = espbt::ClientState::ESTABLISHED;
char char_buf[esp32_ble::UUID_STR_LEN];
esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf),
ble_client_->address_str());
break;
}
default:
break;
}
}
private:
BLEClient *ble_client_;
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
union Value {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash
} value_;
espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_;
std::tuple<Ts...> var_{};
uint16_t char_handle_{};
esp_gatt_char_prop_t char_props_{};
esp_gatt_write_type_t write_type_{};
};
template<typename... Ts> class BLEClientPasskeyReplyAction final : public Action<Ts...> {
public:
BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
@@ -1,103 +0,0 @@
// Neutral twins of the shared ble_client automations. Class names, namespace,
// and codegen-visible signatures are IDENTICAL to automation.h so generated
// main.cpp compiles against whichever engine the build gates in; only the
// internals differ (client callbacks and the neutral node interface instead
// of raw gattc events). The Bluedroid-security automations (passkey, numeric
// comparison, remove bond) have no neutral equivalent and stay esp32-only.
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_BLE_CLIENT_GATT_NODES) && !defined(USE_BLE_CLIENT_LEGACY_ENGINE)
#include <tuple>
#include "ble_client_gatt.h"
#include "esphome/core/automation.h"
namespace esphome::ble_client {
template<typename... Ts> class BLEClientConnectAction final : public Action<Ts...> {
public:
BLEClientConnectAction(BLEClient *ble_client) {
ble_client_ = ble_client;
ble_client->add_on_connect_callback([this]() {
if (this->num_running_ != 0)
this->play_next_tuple_(this->var_);
});
// A connect attempt that dies (or a later disconnect) terminates the
// chain - unless a fresh attempt is already in flight, meaning a user
// trigger on this same list started this action (an on_disconnect that
// reconnects) and the wait belongs to that attempt. Legacy split these:
// it aborted on DISCONNECT_EVT, the trigger fired on the later CLOSE_EVT.
auto terminate = [this]() {
if (this->num_running_ != 0 && this->ble_client_->idle())
this->stop_complex();
};
ble_client->add_on_connect_failed_callback(terminate);
ble_client->add_on_disconnect_callback(terminate);
}
// not used since we override play_complex_
void play(const Ts &...x) override {}
void play_complex(const Ts &...x) override {
// it makes no sense to have multiple instances of this running at the
// same time; cancel a re-trigger while still running.
if (this->num_running_ != 0) {
this->stop_complex();
return;
}
this->num_running_++;
if (this->ble_client_->connected()) {
this->play_next_(x...);
} else {
this->var_ = std::make_tuple(x...);
// No-op while already connecting; the callback resolves the wait.
this->ble_client_->connect();
}
}
private:
BLEClient *ble_client_;
std::tuple<Ts...> var_{};
};
template<typename... Ts> class BLEClientDisconnectAction final : public Action<Ts...> {
public:
BLEClientDisconnectAction(BLEClient *ble_client) {
ble_client_ = ble_client;
// Both terminal outcomes resolve the wait: a completed teardown and a
// connect attempt that died on the way down.
ble_client->add_on_disconnect_callback([this]() {
if (this->num_running_ != 0)
this->play_next_tuple_(this->var_);
});
ble_client->add_on_connect_failed_callback([this]() {
if (this->num_running_ != 0)
this->play_next_tuple_(this->var_);
});
}
// not used since we override play_complex_
void play(const Ts &...x) override {}
void play_complex(const Ts &...x) override {
this->num_running_++;
if (this->ble_client_->idle()) {
this->play_next_(x...);
} else {
this->var_ = std::make_tuple(x...);
this->ble_client_->disconnect();
}
}
private:
BLEClient *ble_client_;
std::tuple<Ts...> var_{};
};
} // namespace esphome::ble_client
#endif // USE_BLE_CLIENT_GATT_NODES && !USE_BLE_CLIENT_LEGACY_ENGINE
+3 -278
View File
@@ -2,16 +2,10 @@
#include "esphome/components/esp32_ble_client/ble_client_base.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_BLE_CLIENT_LEGACY_ENGINE
#ifdef USE_BLE_CLIENT_GATT_NODES
#include "esphome/components/bluetooth_connection/bluetooth_connection.h"
#include "esphome/components/bluetooth_connection/gatt_service_table_bluedroid.h"
#endif
#ifdef USE_ESP32
namespace esphome::ble_client {
@@ -36,10 +30,6 @@ void BLEClient::dump_config() {
bool BLEClient::parse_device(const espbt::ESPBTDevice &device) {
if (!this->enabled)
return false;
#ifdef USE_BLE_CLIENT_GATT_NODES
if (device.address_uint64() == this->address_ && this->gatt_backoff_.holding_off())
return false;
#endif
return BLEClientBase::parse_device(device);
}
@@ -50,65 +40,24 @@ void BLEClient::set_enabled(bool enabled) {
if (!enabled) {
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str());
this->disconnect();
return;
}
#ifdef USE_BLE_CLIENT_GATT_NODES
// A re-enable clears the backoff (neutral-engine parity).
this->gatt_backoff_.reset();
#endif
}
bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
esp_ble_gattc_cb_param_t *param) {
#ifdef USE_BLE_CLIENT_GATT_NODES
// Bridge-initiated registrations bypass the base's REG_FOR_NOTIFY handling:
// its automatic CCCD write would double the node's own.
// Handle-keyed: mixed legacy/neutral subscriptions to one characteristic
// are unsupported during the migration window.
if (event == ESP_GATTC_REG_FOR_NOTIFY_EVT && esp_gattc_if == this->gattc_if_ &&
this->take_pending_gatt_reg_(param->reg_for_notify.handle)) {
if (this->pending_notify_regs_ > 0)
this->pending_notify_regs_--;
int err = param->reg_for_notify.status == ESP_GATT_OK ? 0 : param->reg_for_notify.status;
this->notify_state_to_gatt_nodes_(param->reg_for_notify.handle, true, err);
// A retiring last registration must still release the cache.
this->maybe_release_services_();
return true;
}
#endif
if (!BLEClientBase::gattc_event_handler(event, esp_gattc_if, param))
return false;
#ifdef USE_BLE_CLIENT_GATT_NODES
// Before the legacy fan-out so gatt nodes resolve before any trigger fires.
if (!this->gatt_nodes_.empty()) {
if (event == ESP_GATTC_SEARCH_CMPL_EVT) {
// A failed discovery tears the link down; the on_connect trigger must
// not fire into the teardown.
if (!this->handle_gatt_search_cmpl_(param->search_cmpl.status))
return true;
} else {
this->dispatch_gatt_event_(event, param);
}
}
#endif
for (auto *node : this->nodes_)
node->gattc_event_handler(event, esp_gattc_if, param);
// The automations run after the nodes; on_connect runs before the release so it can still use the services
if (event == ESP_GATTC_SEARCH_CMPL_EVT)
this->connect_callbacks_.call();
this->maybe_release_services_();
if (event == ESP_GATTC_CLOSE_EVT)
this->disconnect_callbacks_.call();
return true;
}
void BLEClient::maybe_release_services_() {
// The release frees the GATT cache that BLEClientBase's CCCD lookup still needs.
// The last REG_FOR_NOTIFY event clears the counter before node dispatch, so the release still runs here.
if (!this->services_.empty() && !this->notify_registration_pending() && this->all_nodes_established_()) {
this->release_services();
ESP_LOGD(TAG, "All clients established, services released");
}
return true;
}
void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
@@ -116,19 +65,10 @@ void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_p
for (auto *node : this->nodes_)
node->gap_event_handler(event, param);
#ifdef USE_BLE_CLIENT_GATT_NODES
if (event == ESP_GAP_BLE_AUTH_CMPL_EVT && this->check_addr(param->ble_security.auth_cmpl.bd_addr)) {
int status = param->ble_security.auth_cmpl.success ? 0 : param->ble_security.auth_cmpl.fail_reason;
for (auto *node : this->gatt_nodes_)
node->on_pairing_result(status);
}
#endif
}
void BLEClient::set_state(espbt::ClientState state) {
BLEClientBase::set_state(state);
// ESTABLISHED never flows through here; gatt nodes are promoted after the
// on_connected fan-out.
for (auto &node : nodes_)
node->node_state = state;
}
@@ -143,221 +83,6 @@ bool BLEClient::all_nodes_established_() {
return true;
}
#ifdef USE_BLE_CLIENT_GATT_NODES
void BLEClient::register_gatt_node(BLEClientNode *node) {
// Parent before the capacity check so a dropped node still has a usable
// parent() (neutral-engine parity).
node->set_ble_client_parent(this);
if (this->gatt_nodes_.size() == ESPHOME_BLE_CLIENT_MAX_NODES) {
// push_back past capacity is a silent no-op; an undersized slot count
// must be loud at boot, not an unresolvable node at runtime.
ESP_LOGE(TAG, "[%s] Node capacity exceeded; node dropped", this->address_str());
this->status_set_error(LOG_STR("node capacity exceeded"));
return;
}
this->gatt_nodes_.push_back(node);
// nodes_ covers the shared state bookkeeping; gatt_nodes_ is the neutral
// fan-out subset.
this->register_ble_node(node);
}
int BLEClient::find_pending_gatt_reg_(uint16_t handle) const {
for (uint8_t i = 0; i < this->pending_gatt_reg_count_; i++) {
if (this->pending_gatt_regs_[i] == handle)
return i;
}
return -1;
}
bool BLEClient::take_pending_gatt_reg_(uint16_t handle) {
int i = this->find_pending_gatt_reg_(handle);
if (i < 0)
return false;
// No duplicates (notify_characteristic refuses a re-push); swap-with-last.
this->pending_gatt_regs_[i] = this->pending_gatt_regs_[--this->pending_gatt_reg_count_];
return true;
}
void BLEClient::notify_state_to_gatt_nodes_(uint16_t handle, bool enabled, int error) {
if (error != 0) {
ESP_LOGW(TAG, "[%s] Notify %s on handle 0x%04x failed, status=%d", this->address_str(),
enabled ? "enable" : "disable", handle, error);
}
for (auto *node : this->gatt_nodes_)
node->on_notify_state(handle, enabled, error);
}
void BLEClient::dispatch_gatt_event_(esp_gattc_cb_event_t event, esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_READ_DESCR_EVT: {
bool ok = param->read.status == ESP_GATT_OK;
if (!ok) {
// Breadcrumb even when no node claims the handle.
ESP_LOGD(TAG, "[%s] Read on handle 0x%04x completed with status %d", this->address_str(), param->read.handle,
param->read.status);
}
for (auto *node : this->gatt_nodes_) {
node->on_read_result(param->read.handle, ok ? param->read.value : nullptr, ok ? param->read.value_len : 0,
ok ? 0 : param->read.status);
}
break;
}
case ESP_GATTC_WRITE_CHAR_EVT:
case ESP_GATTC_WRITE_DESCR_EVT:
if (param->write.status != ESP_GATT_OK) {
// Breadcrumb even when no node claims the handle.
ESP_LOGD(TAG, "[%s] Write on handle 0x%04x completed with status %d", this->address_str(), param->write.handle,
param->write.status);
}
for (auto *node : this->gatt_nodes_) {
node->on_write_result(param->write.handle, param->write.status == ESP_GATT_OK ? 0 : param->write.status);
}
break;
case ESP_GATTC_NOTIFY_EVT:
for (auto *node : this->gatt_nodes_) {
node->on_notify(param->notify.handle, param->notify.value, param->notify.value_len);
}
break;
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT:
// The base does no CCCD work for unregister; no interception needed.
this->notify_state_to_gatt_nodes_(
param->unreg_for_notify.handle, false,
param->unreg_for_notify.status == ESP_GATT_OK ? 0 : param->unreg_for_notify.status);
break;
default:
break;
}
}
bool BLEClient::handle_gatt_search_cmpl_(esp_gatt_status_t status) {
// The base ignores the search status; the neutral contract must not.
uint16_t service_total = 0;
bool counted = status == ESP_GATT_OK && bluetooth_connection::BluedroidServiceTable::count_services(
this->gattc_if_, this->conn_id_, &service_total);
if (!counted || service_total == 0) {
// A failed search poisons the whole discovery, legacy nodes included.
ESP_LOGW(TAG, "[%s] Discovery failed (status=%d, services=%u)", this->address_str(), status, service_total);
this->gatt_backoff_.register_failure(this->address_str());
this->disconnect();
return false;
}
// Stack-owned; nodes copy their handles during on_connected().
bluetooth_connection::BluedroidServiceTable table;
if (!table.build(this->gattc_if_, this->conn_id_, service_total, this->connection_index_)) {
if (!this->has_legacy_nodes_()) {
ESP_LOGW(TAG, "[%s] Service table build failed; treating as failed discovery", this->address_str());
this->gatt_backoff_.register_failure(this->address_str());
this->disconnect();
return false;
}
// Only the table build failed; legacy nodes read the base's services_
// and keep the link. Gatt nodes catch the next connection.
ESP_LOGW(TAG, "[%s] Service table build failed; gatt nodes skip this connection", this->address_str());
this->status_set_warning(LOG_STR("gatt nodes inactive: service table build failed"));
// A peer whose database never walks must not retry the build at
// advertisement cadence once the legacy nodes drop the link.
this->gatt_backoff_.register_failure(this->address_str());
} else {
this->gatt_connected_ = true;
auto view = table.view();
for (auto *node : this->gatt_nodes_) {
node->on_connected(view);
if (this->state() != espbt::ClientState::ESTABLISHED) {
// The node tore the link down; remaining nodes get on_disconnected
// with no preceding on_connected, so leave a trace of why.
ESP_LOGW(TAG, "[%s] A node aborted the connection during setup", this->address_str());
return false;
}
}
this->gatt_backoff_.reset();
this->status_clear_warning();
}
// Promote so the legacy release condition can fire.
for (auto *node : this->gatt_nodes_)
node->node_state = espbt::ClientState::ESTABLISHED;
return true;
}
void BLEClient::on_disconnect_complete(esp_err_t reason) {
this->pending_gatt_reg_count_ = 0;
if (!this->gatt_connected_)
return; // Never-established links report nothing (neutral parity).
this->gatt_connected_ = false;
for (auto *node : this->gatt_nodes_)
node->on_disconnected();
}
int BLEClient::check_and_log_error_(const char *operation, esp_err_t err) {
if (err != ESP_OK)
this->log_gattc_warning_(operation, err);
return err;
}
int BLEClient::write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
if (this->conn_id_ == UNSET_CONN_ID)
return ble_device_base::GATT_ERR_NOT_CONNECTED;
return this->check_and_log_error_(
"esp_ble_gattc_write_char",
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, len, const_cast<uint8_t *>(data),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP,
ESP_GATT_AUTH_REQ_NONE));
}
int BLEClient::read_characteristic(uint16_t handle) {
if (this->conn_id_ == UNSET_CONN_ID)
return ble_device_base::GATT_ERR_NOT_CONNECTED;
return this->check_and_log_error_("esp_ble_gattc_read_char", esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_,
handle, ESP_GATT_AUTH_REQ_NONE));
}
int BLEClient::read_descriptor(uint16_t handle) {
if (this->conn_id_ == UNSET_CONN_ID)
return ble_device_base::GATT_ERR_NOT_CONNECTED;
return this->check_and_log_error_(
"esp_ble_gattc_read_char_descr",
esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE));
}
int BLEClient::write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) {
if (this->conn_id_ == UNSET_CONN_ID)
return ble_device_base::GATT_ERR_NOT_CONNECTED;
return this->check_and_log_error_(
"esp_ble_gattc_write_char_descr",
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, handle, len, const_cast<uint8_t *>(data),
ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE));
}
int BLEClient::notify_characteristic(uint16_t handle, bool enable) {
if (this->conn_id_ == UNSET_CONN_ID)
return ble_device_base::GATT_ERR_NOT_CONNECTED;
if (enable) {
if (this->find_pending_gatt_reg_(handle) >= 0) {
// ESP_OK: the in-flight registration's completion fans out to all nodes.
ESP_LOGW(TAG, "[%s] Notify registration already pending for handle 0x%04x", this->address_str(), handle);
return ESP_OK;
}
if (this->pending_gatt_reg_count_ == MAX_PENDING_NOTIFY_REGS) {
// An untracked registration would let the base's auto-CCCD through.
ESP_LOGE(TAG, "[%s] Too many pending notify registrations", this->address_str());
return ble_device_base::GATT_ERR_NO_MEMORY;
}
// The base helper's pending count holds the service-release until the
// (intercepted) completion.
esp_err_t err = this->register_for_notify(handle);
if (err == ESP_OK)
this->pending_gatt_regs_[this->pending_gatt_reg_count_++] = handle;
return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
}
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify",
esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle));
}
int BLEClient::unpair() { return bluetooth_connection::unpair_device(this->get_address()); }
#endif // USE_BLE_CLIENT_GATT_NODES
} // namespace esphome::ble_client
#endif
+33 -63
View File
@@ -1,20 +1,18 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BLE_CLIENT_LEGACY_ENGINE
#include "ble_client_node.h"
#include "connect_backoff.h"
#include "esphome/components/esp32_ble_client/ble_client_base.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32
#include <esp_bt_defs.h>
#include <esp_gap_ble_api.h>
#include <esp_gatt_common_api.h>
#include <esp_gattc_api.h>
#include <array>
#include <string>
#include <vector>
namespace esphome::ble_client {
@@ -23,6 +21,34 @@ namespace espbt = esphome::esp32_ble_tracker;
using namespace esp32_ble_client;
class BLEClient;
class BLEClientNode {
public:
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {};
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {}
virtual void loop() {}
void set_address(uint64_t address) { address_ = address; }
espbt::ESPBTClient *client;
// This should be transitioned to Established once the node no longer needs
// the services/descriptors/characteristics of the parent client. This will
// allow some memory to be freed.
// The parent frees the peer's GATT cache once every node reports Established.
// Never report Established while an operation that reads that cache is outstanding.
// - esp_ble_gattc_register_for_notify() completes asynchronously.
// - Register from ESP_GATTC_SEARCH_CMPL_EVT, then set this from ESP_GATTC_REG_FOR_NOTIFY_EVT.
// - BLEClientBase::register_for_notify() holds the release until the registration completes.
espbt::ClientState node_state;
BLEClient *parent() { return this->parent_; }
void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; }
protected:
BLEClient *parent_;
uint64_t address_;
};
class BLEClient final : public BLEClientBase {
public:
void setup() override;
@@ -38,75 +64,19 @@ class BLEClient final : public BLEClientBase {
void set_enabled(bool enabled);
void register_ble_node(BLEClientNode *node) {
node->client = this;
node->set_ble_client_parent(this);
this->nodes_.push_back(node);
}
// on_connect fires once discovery completes; on_disconnect on CLOSE_EVT, so only after a completed open
template<typename F> void add_on_connect_callback(F &&callback) {
this->connect_callbacks_.add(std::forward<F>(callback));
}
template<typename F> void add_on_disconnect_callback(F &&callback) {
this->disconnect_callbacks_.add(std::forward<F>(callback));
}
bool enabled;
void set_state(espbt::ClientState state) override;
#ifdef USE_BLE_CLIENT_GATT_NODES
// ---- the neutral node surface (signatures shared with the non-esp32
// engine, so nodes on the neutral interface compile against either) ----
void register_gatt_node(BLEClientNode *node);
bool idle() const { return this->state() == espbt::ClientState::IDLE; }
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response);
int read_characteristic(uint16_t handle);
int read_descriptor(uint16_t handle);
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len);
/// Local registration only; per the neutral contract the CCCD write is the
/// node's job (the legacy auto-CCCD is suppressed for these handles).
int notify_characteristic(uint16_t handle, bool enable);
// pair() comes from BLEClientBase, matching the neutral engine's.
int unpair();
#endif
protected:
bool all_nodes_established_();
void maybe_release_services_();
#ifdef USE_BLE_CLIENT_GATT_NODES
int check_and_log_error_(const char *operation, esp_err_t err);
int find_pending_gatt_reg_(uint16_t handle) const;
void notify_state_to_gatt_nodes_(uint16_t handle, bool enabled, int error);
void dispatch_gatt_event_(esp_gattc_cb_event_t event, esp_ble_gattc_cb_param_t *param);
// False = failed discovery: the link comes down and the caller suppresses
// the legacy fan-out.
bool handle_gatt_search_cmpl_(esp_gatt_status_t status);
bool take_pending_gatt_reg_(uint16_t handle);
void on_disconnect_complete(esp_err_t reason) override;
#endif
std::vector<BLEClientNode *> nodes_;
LazyCallbackManager<void()> connect_callbacks_;
LazyCallbackManager<void()> disconnect_callbacks_;
#ifdef USE_BLE_CLIENT_GATT_NODES
// Raise if a migrated node needs more concurrent registrations.
static constexpr uint8_t MAX_PENDING_NOTIFY_REGS = 4;
// Nodes on the neutral surface; fed the translated callbacks and
// auto-established after the on_connected fan-out. Every gatt node is
// also in nodes_ (registration pushes into both).
StaticVector<BLEClientNode *, ESPHOME_BLE_CLIENT_MAX_NODES> gatt_nodes_;
bool has_legacy_nodes_() const { return this->nodes_.size() > this->gatt_nodes_.size(); }
// Reconnect backoff after materializer failures.
ConnectBackoff gatt_backoff_;
// Bridge-initiated notify registrations awaiting REG_FOR_NOTIFY_EVT.
uint16_t pending_gatt_regs_[MAX_PENDING_NOTIFY_REGS];
uint8_t pending_gatt_reg_count_{0};
// on_connected fan-out started; on_disconnected is owed at teardown.
bool gatt_connected_{false};
#endif
};
} // namespace esphome::ble_client
@@ -1,261 +0,0 @@
#include "ble_client_gatt.h"
#if defined(USE_BLE_CLIENT_GATT_NODES) && !defined(USE_BLE_CLIENT_LEGACY_ENGINE)
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::ble_client {
ESPHOME_LOG_TAG(TAG, "ble_client");
void BLEClient::register_ble_node(BLEClientNode *node) {
node->set_ble_client_parent(this);
if (this->nodes_.size() == ESPHOME_BLE_CLIENT_MAX_NODES) {
// push_back past capacity is a silent no-op; an undersized slot count
// must be loud at boot, not an unresolvable node at runtime.
ESP_LOGE(TAG, "[%s] Node capacity exceeded; node dropped", this->address_str_);
this->status_set_error(LOG_STR("node capacity exceeded"));
return;
}
this->nodes_.push_back(node);
}
void BLEClient::set_address(uint64_t address) {
this->address_ = address;
uint8_t mac[6];
ble_device_base::uint64_to_mac_msb_first(address, mac);
format_mac_addr_upper(mac, this->address_str_);
}
void BLEClient::set_enabled(bool enabled) {
if (enabled == this->enabled)
return;
ESP_LOGI(TAG, "[%s] %s", this->address_str_, enabled ? "Enabled" : "Disabled");
this->enabled = enabled;
if (!enabled) {
this->disconnect();
return;
}
// A re-enable clears the backoff; the next sighting connects (legacy
// parity: enabling does not itself connect).
this->backoff_.reset();
}
bool BLEClient::parse_device(const ble_device_base::ESPBTDevice &device) {
if (device.address_uint64() != this->address_)
return false;
// The sighting is the source of truth for the address type.
this->address_type_ = device.get_address_type();
this->address_type_known_ = true;
if (!this->enabled || !this->auto_connect_ || this->state_ != State::IDLE)
return true;
if (this->backoff_.holding_off())
return true;
this->attempt_connect_();
return true;
}
void BLEClient::connect() {
if (this->state_ != State::IDLE) {
ESP_LOGD(TAG, "[%s] Connect requested while busy, ignoring", this->address_str_);
return;
}
// An absent peer can inhibit scanning for the backend's full connect
// timeout, so this is worth a breadcrumb - but it is a supported action.
ESP_LOGI(TAG, "[%s] Connecting on request", this->address_str_);
if (!this->address_type_known_) {
// Legacy parity: without a sighting the address type defaults to
// public, which never matches a random-static peer.
ESP_LOGW(TAG, "[%s] No sighting yet; assuming a public address type", this->address_str_);
}
this->attempt_connect_();
}
void BLEClient::attempt_connect_() {
int err = this->backend_->connect(this->address_, this->address_type_);
if (err != 0) {
// A refused connect never produces a callback: stay idle, charge the
// backoff, and resolve any waiting connect action through the failure
// path so its chain terminates.
ESP_LOGW(TAG, "[%s] Connect refused, err=%d", this->address_str_, err);
this->backoff_.register_failure(this->address_str_);
this->defer([this]() { this->connect_failed_callbacks_.call(); });
return;
}
ESP_LOGD(TAG, "[%s] Connecting", this->address_str_);
this->state_ = State::CONNECTING;
}
void BLEClient::disconnect() {
if (this->state_ == State::IDLE) {
ESP_LOGD(TAG, "[%s] Disconnect requested while idle, ignoring", this->address_str_);
return;
}
if (this->cancel_requested_) {
// The accepted teardown still reports; a refused second gatt_disconnect()
// would settle the link twice (phantom connect failure, backoff charge).
ESP_LOGD(TAG, "[%s] Disconnect already in flight, ignoring", this->address_str_);
return;
}
// A deliberate teardown's failure report must not feed the backoff.
this->cancel_requested_ = true;
int err = this->backend_->gatt_disconnect();
if (err != 0) {
// Refused synchronously: backend and client disagree about the link
// state. Warn, then settle through the deliberate-cancel path.
ESP_LOGW(TAG, "[%s] Disconnect refused, err=%d; settling locally", this->address_str_, err);
this->on_connection_state(false, 0, err);
}
}
void BLEClient::on_connection_state(bool connected, uint16_t mtu, int error) {
if (connected) {
this->state_ = State::DISCOVERING;
int discover_err = this->backend_->discover_services();
if (discover_err != 0) {
// Synchronous refusal: no discovery completion will follow.
ESP_LOGW(TAG, "[%s] Service discovery refused, err=%d", this->address_str_, discover_err);
this->backoff_.register_failure(this->address_str_);
// Deliberate teardown: its report must not charge the backoff again.
this->disconnect();
}
return;
}
bool was_connected = this->state_ == State::CONNECTED;
bool cancelled = this->cancel_requested_;
this->cancel_requested_ = false;
this->state_ = State::IDLE;
if (was_connected) {
ESP_LOGI(TAG, "[%s] Disconnected, status=%d", this->address_str_, error);
for (auto *node : this->nodes_) {
node->on_disconnected();
}
// Continuations leave the backend's event-drain stack first.
this->defer([this]() { this->disconnect_callbacks_.call(); });
} else {
if (cancelled) {
// status carries the refusal code when the teardown settled
// synchronously; 0 on a backend-completed cancel.
ESP_LOGD(TAG, "[%s] Connect attempt cancelled, status=%d", this->address_str_, error);
} else {
ESP_LOGW(TAG, "[%s] Connect failed, status=%d", this->address_str_, error);
this->backoff_.register_failure(this->address_str_);
}
this->defer([this]() { this->connect_failed_callbacks_.call(); });
}
}
void BLEClient::on_service_discovery_done(int error) {
if (this->state_ != State::DISCOVERING || this->cancel_requested_) {
// Raced a teardown; the terminal report settles the link.
ESP_LOGD(TAG, "[%s] Discovery completed during teardown, ignoring", this->address_str_);
this->backend_->release_services();
return;
}
if (error != 0) {
ESP_LOGW(TAG, "[%s] Service discovery failed, status=%d", this->address_str_, error);
this->backoff_.register_failure(this->address_str_);
// The teardown is deliberate: do not charge the backoff again for its
// connection report.
this->disconnect();
return;
}
ble_device_base::GattServiceTable table{};
if (!this->nodes_.empty()) {
// Materialize only when a node will read it: a client with no nodes
// would pay the build/free cycle on every (re)connect for nothing.
table = this->backend_->get_service_table();
if (table.service_count == 0) {
// A failed materialization is indistinguishable from a service-less
// peer, and a real GATT peer always exposes at least GAP/GATT: fail
// the discovery before CONNECTED so the teardown resolves through
// connect_failed, never a spurious on_disconnect.
ESP_LOGW(TAG, "[%s] Service table is empty; treating as failed discovery", this->address_str_);
this->backend_->release_services();
this->backoff_.register_failure(this->address_str_);
this->disconnect();
return;
}
}
// CONNECTED before the fan-out so nodes may consult connected() from
// their own on_connected().
this->state_ = State::CONNECTED;
for (auto *node : this->nodes_) {
node->on_connected(table);
if (this->state_ != State::CONNECTED || this->cancel_requested_) {
// A node tore the link down mid-fan-out: on_disconnect fires with no
// preceding on_connect, so leave a trace of why.
ESP_LOGW(TAG, "[%s] A node aborted the connection during setup", this->address_str_);
this->backend_->release_services();
return;
}
}
this->backend_->release_services();
this->backoff_.reset();
ESP_LOGI(TAG, "[%s] Connected", this->address_str_);
this->defer([this]() { this->connect_callbacks_.call(); });
}
void BLEClient::on_write_result(uint16_t handle, int error) {
if (error != 0) {
// Breadcrumb even when no node claims the handle.
ESP_LOGD(TAG, "[%s] Write on handle 0x%04x completed with status %d", this->address_str_, handle, error);
}
for (auto *node : this->nodes_) {
node->on_write_result(handle, error);
}
}
void BLEClient::on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) {
if (error != 0) {
// Breadcrumb even when no node claims the handle.
ESP_LOGD(TAG, "[%s] Read on handle 0x%04x completed with status %d", this->address_str_, handle, error);
}
for (auto *node : this->nodes_) {
node->on_read_result(handle, data, len, error);
}
}
void BLEClient::on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) {
// Every node sees every notification and filters by handle (legacy parity).
for (auto *node : this->nodes_) {
node->on_notify(handle, data, len);
}
}
void BLEClient::on_notify_state(uint16_t handle, bool enabled, int error) {
if (error != 0) {
ESP_LOGW(TAG, "[%s] Notify %s on handle 0x%04x failed, status=%d", this->address_str_,
enabled ? "enable" : "disable", handle, error);
}
for (auto *node : this->nodes_) {
node->on_notify_state(handle, enabled, error);
}
}
void BLEClient::on_pairing_result(int status) {
if (status != 0) {
ESP_LOGW(TAG, "[%s] Pairing failed, status=%d", this->address_str_, status);
} else {
ESP_LOGI(TAG, "[%s] Paired", this->address_str_);
}
for (auto *node : this->nodes_) {
node->on_pairing_result(status);
}
}
void BLEClient::dump_config() {
ESP_LOGCONFIG(TAG,
"BLE Client:\n"
" Address: %s\n"
" Auto connect: %s",
this->address_str_, YESNO(this->auto_connect_));
if (this->enabled && this->state_ == State::IDLE) {
ESP_LOGCONFIG(TAG, " Waiting for an advertisement from the device");
}
}
} // namespace esphome::ble_client
#endif // USE_BLE_CLIENT_GATT_NODES && !USE_BLE_CLIENT_LEGACY_ENGINE
@@ -1,151 +0,0 @@
// Platform-neutral ble_client engine on the ble_device_base GATT contract.
//
// Compiled on every platform with a GATT backend except esp32, which keeps
// the legacy BLEClientBase engine (ble_client.h) until its raw-gattc node
// family migrates - the exclusive gates make the same class names resolve to
// exactly one definition per build, so codegen is shared.
//
// Connects are sighting-gated like the legacy engine: the client is a parsed
// advertisement listener, captures the peer's address type from the sighting,
// and asks the backend to connect only when enabled and idle.
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_BLE_CLIENT_GATT_NODES) && !defined(USE_BLE_CLIENT_LEGACY_ENGINE)
#include "ble_client_node.h"
#include "connect_backoff.h"
#include "esphome/components/ble_device_base/ble_device.h"
#include "esphome/components/ble_device_base/ble_gatt_client.h"
#include "esphome/components/bluetooth_connection/bluetooth_connection.h"
#include "esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <cstdint>
#include <functional>
namespace esphome::ble_client {
class BLEClient : public Component,
public ble_device_base::ESPBTDeviceListener,
public ble_device_base::GattClientListener {
public:
void dump_config() override;
// Public field for legacy parity (the switch platform republishes it).
bool enabled{true};
void set_backend(ble_device_base::BLEGattConnection *backend) {
this->backend_ = backend;
backend->set_listener(this);
}
void set_address(uint64_t address);
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
void set_enabled(bool enabled);
const char *address_str() const { return this->address_str_; }
void register_ble_node(BLEClientNode *node);
// One registration spelling shared with the esp32 engine's bridge.
void register_gatt_node(BLEClientNode *node) { this->register_ble_node(node); }
// An accepted teardown already owns the link: the pending report cannot be
// a connect. state_ stays put so that report still fires on_disconnect.
bool connected() const { return this->state_ == State::CONNECTED && !this->cancel_requested_; }
bool idle() const { return this->state_ == State::IDLE; }
/// Action-initiated connect (no sighting needed; uses the last captured
/// address type, public until a sighting arrives). No-op unless idle.
void connect();
void disconnect();
/// Legacy-named deferral used by the automation twins: neutral listener
/// callbacks run inside the backend's event drain, so automation chain
/// continuations must leave that stack first.
void run_later(std::function<void()> &&f) { this->defer(std::move(f)); } // NOLINT
// Backend ops for nodes and actions - the frozen node-facing surface.
// Only write_characteristic has an in-tree caller; subscribing means
// notify_characteristic plus a CCCD write_descriptor (the caller's job
// per the contract).
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
return this->backend_->write_characteristic(handle, data, len, response);
}
int read_characteristic(uint16_t handle) { return this->backend_->read_characteristic(handle); }
int read_descriptor(uint16_t handle) { return this->backend_->read_descriptor(handle); }
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) {
return this->backend_->write_descriptor(handle, data, len);
}
int notify_characteristic(uint16_t handle, bool enable) {
return this->backend_->notify_characteristic(handle, enable);
}
int pair() { return this->backend_->pair(); }
int unpair() { return bluetooth_connection::unpair_device(this->address_); }
// Automation callback registration.
template<typename F> void add_on_connect_callback(F &&callback) {
this->connect_callbacks_.add(std::forward<F>(callback));
}
template<typename F> void add_on_disconnect_callback(F &&callback) {
this->disconnect_callbacks_.add(std::forward<F>(callback));
}
// Fired when a connect attempt dies before being established; the user
// on_disconnect trigger deliberately does NOT fire here (legacy parity).
template<typename F> void add_on_connect_failed_callback(F &&callback) {
this->connect_failed_callbacks_.add(std::forward<F>(callback));
}
// ---- ble_device_base::ESPBTDeviceListener ----
bool parse_device(const ble_device_base::ESPBTDevice &device) override;
// ---- ble_device_base::GattClientListener ----
void on_connection_state(bool connected, uint16_t mtu, int error) override;
void on_service_discovery_done(int error) override;
void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) override;
void on_write_result(uint16_t handle, int error) override;
void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) override;
void on_notify_state(uint16_t handle, bool enabled, int error) override;
void on_pairing_result(int status) override;
protected:
enum class State : uint8_t { IDLE, CONNECTING, DISCOVERING, CONNECTED };
void attempt_connect_();
// Group 1: pointers / containers
ble_device_base::BLEGattConnection *backend_{nullptr};
// Codegen-sized (ESPHOME_BLE_CLIENT_MAX_NODES); filled during setup.
StaticVector<BLEClientNode *, ESPHOME_BLE_CLIENT_MAX_NODES> nodes_;
// Group 2: 8-byte types
uint64_t address_{0};
// Group 3: callback managers (pointer-sized when empty)
LazyCallbackManager<void()> connect_callbacks_;
LazyCallbackManager<void()> disconnect_callbacks_;
LazyCallbackManager<void()> connect_failed_callbacks_;
// Group 4: 4-byte types
// Backoff so an undiscoverable database or a dead peer cannot produce a
// battery-draining connect loop.
ConnectBackoff backoff_;
// Group 5: arrays
char address_str_[MAC_ADDRESS_PRETTY_BUFFER_SIZE]{};
// Group 6: 1-byte types
State state_{State::IDLE};
uint8_t address_type_{0}; // BLE_ADDR_TYPE_*, captured from the sighting
// Distinguishes a captured public type from the never-sighted default.
bool address_type_known_{false};
bool auto_connect_{true};
// A user-initiated teardown in flight; its failure report is not a
// connect failure and must not feed the backoff.
bool cancel_requested_{false};
};
} // namespace esphome::ble_client
#endif // USE_BLE_CLIENT_GATT_NODES && !USE_BLE_CLIENT_LEGACY_ENGINE
@@ -1,68 +0,0 @@
// The single BLEClientNode both ble_client engines share. The neutral
// callback surface is the one interface node components build on; the raw
// esp32 surface below it remains for components that have not migrated yet.
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BLE_GATT_CLIENT
#include "esphome/components/ble_device_base/ble_gatt_client.h"
#endif
#ifdef USE_BLE_CLIENT_LEGACY_ENGINE
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include <esp_gap_ble_api.h>
#include <esp_gattc_api.h>
#endif
#include <cstdint>
namespace esphome::ble_client {
class BLEClient;
class BLEClientNode {
public:
#ifdef USE_BLE_CLIENT_GATT_NODES
// Neutral surface, delivered by both engines. The table is borrowed: copy
// handles during on_connected(). All nodes see all completions; filter by
// handle.
// A node that disconnects from inside on_connected() aborts the fan-out;
// the user's on_disconnect may then fire without a preceding on_connect.
virtual void on_connected(const ble_device_base::GattServiceTable &table) {}
virtual void on_disconnected() {}
virtual void on_notify(uint16_t handle, const uint8_t *data, uint16_t len) {}
// One in-flight registration per handle; its completion fans out to every
// node, so a refused duplicate request still sees on_notify_state.
virtual void on_notify_state(uint16_t handle, bool enabled, int error) {}
virtual void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) {}
virtual void on_write_result(uint16_t handle, int error) {}
virtual void on_pairing_result(int status) {}
#endif
#ifdef USE_BLE_CLIENT_LEGACY_ENGINE
// Legacy raw surface; components overriding these need the legacy engine
// until migrated to the neutral surface above.
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {}
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {}
virtual void loop() {}
// This should be transitioned to Established once the node no longer needs
// the services/descriptors/characteristics of the parent client. This will
// allow some memory to be freed.
// The parent frees the peer's GATT cache once every node reports Established.
// Never report Established while an operation that reads that cache is outstanding.
// - esp_ble_gattc_register_for_notify() completes asynchronously.
// - Register from ESP_GATTC_SEARCH_CMPL_EVT, then set this from ESP_GATTC_REG_FOR_NOTIFY_EVT.
// - BLEClientBase::register_for_notify() holds the release until the registration completes.
esp32_ble_tracker::ClientState node_state;
#endif
BLEClient *parent() const { return this->parent_; }
void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; }
protected:
BLEClient *parent_{nullptr};
};
} // namespace esphome::ble_client
@@ -1,172 +0,0 @@
// The ble_client.ble_write action: a node on the platform-neutral interface,
// so one implementation serves both engines (the esp32 bridge and the
// neutral engine).
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BLE_CLIENT_GATT_NODES
#include <tuple>
#include <vector>
// One of the two engine headers resolves per build.
#include "ble_client.h"
#include "ble_client_gatt.h"
#include "ble_client_node.h"
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::ble_client {
static const char *const BLE_WRITE_TAG = "ble_client.automation";
// Maximum bytes to log in hex format for BLE writes (many logging buffers are 256 chars)
static constexpr size_t BLE_WRITE_MAX_LOG_BYTES = 64;
template<typename... Ts> class BLEClientWriteAction final : public Action<Ts...>, public BLEClientNode {
public:
BLEClientWriteAction(BLEClient *ble_client) {
ble_client->register_gatt_node(this);
ble_client_ = ble_client;
}
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = ble_device_base::ESPBTUUID::from_uint16(uuid); }
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = ble_device_base::ESPBTUUID::from_uint32(uuid); }
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = ble_device_base::ESPBTUUID::from_raw(uuid); }
void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = ble_device_base::ESPBTUUID::from_uint16(uuid); }
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = ble_device_base::ESPBTUUID::from_uint32(uuid); }
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = ble_device_base::ESPBTUUID::from_raw(uuid); }
void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
this->value_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
}
// Store pointer to static data in flash (no RAM copy)
void set_value_simple(const uint8_t *data, size_t len) {
this->value_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
}
void play(const Ts &...x) override {}
void play_complex(const Ts &...x) override {
this->num_running_++;
this->var_ = std::make_tuple(x...);
bool result;
if (this->len_ >= 0) {
result = this->write(this->value_.data, this->len_);
} else {
std::vector<uint8_t> value = this->value_.func(x...);
result = this->write(value.data(), value.size());
}
// on write failure, continue the automation chain rather than stopping so
// that e.g. disconnect can work.
if (!result)
this->play_next_(x...);
}
// Initiate the write; the completion arrives in on_write_result. The
// response-less path can complete synchronously inside the call, so the
// handle is armed before the backend is touched.
bool write(const uint8_t *data, size_t len) {
if (!this->ble_client_->connected()) {
esph_log_w(BLE_WRITE_TAG, "Cannot write to BLE characteristic - not connected");
return false;
}
if (!this->resolved_) {
esph_log_w(BLE_WRITE_TAG, "Cannot write to BLE characteristic - characteristic was not resolved");
return false;
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(BLE_WRITE_MAX_LOG_BYTES)];
esph_log_vv(BLE_WRITE_TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len));
#endif
int err = this->ble_client_->write_characteristic(this->char_handle_, data, len, this->write_response_);
if (err != 0) {
esph_log_e(BLE_WRITE_TAG, "Error writing to characteristic: %d!", err);
return false;
}
return true;
}
void on_connected(const ble_device_base::GattServiceTable &table) override {
// Resolution comes from this table only; a re-discovery must not keep a
// stale handle.
this->resolved_ = false;
this->char_handle_ = 0;
const auto *service = ble_device_base::find_service(table, this->service_uuid_);
const auto *chr =
service == nullptr ? nullptr : ble_device_base::find_characteristic(table, *service, this->char_uuid_);
if (chr == nullptr) {
char char_buf[ble_device_base::UUID_STR_LEN];
char service_buf[ble_device_base::UUID_STR_LEN];
esph_log_w(BLE_WRITE_TAG, "Characteristic %s was not found in service %s", this->char_uuid_.to_str(char_buf),
this->service_uuid_.to_str(service_buf));
return;
}
if (chr->properties & ble_device_base::GATT_CHAR_PROP_WRITE) {
this->write_response_ = true;
} else if (chr->properties & ble_device_base::GATT_CHAR_PROP_WRITE_NO_RSP) {
this->write_response_ = false;
} else {
char char_buf[ble_device_base::UUID_STR_LEN];
esph_log_e(BLE_WRITE_TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_str(char_buf));
return;
}
this->char_handle_ = chr->value_handle;
this->resolved_ = true;
char char_buf[ble_device_base::UUID_STR_LEN];
esph_log_d(BLE_WRITE_TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf),
this->ble_client_->address_str());
}
void on_disconnected() override {
this->resolved_ = false;
this->char_handle_ = 0;
if (this->num_running_ != 0)
this->stop_complex();
}
void on_write_result(uint16_t handle, int error) override {
if (this->num_running_ == 0) {
return;
}
if (!this->resolved_ || handle != this->char_handle_) {
// A parked chain waiting on a completion that never matches would
// otherwise stall silently until disconnect.
esph_log_d(BLE_WRITE_TAG, "Write result for handle 0x%04x ignored, waiting on 0x%04x", handle,
this->char_handle_);
return;
}
if (error != 0) {
// Continue the chain (legacy parity) but leave a breadcrumb.
esph_log_w(BLE_WRITE_TAG, "Write completed with status %d", error);
}
this->ble_client_->run_later([this]() { this->play_next_tuple_(this->var_); });
}
private:
BLEClient *ble_client_;
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
union Value {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash
} value_;
ble_device_base::ESPBTUUID service_uuid_;
ble_device_base::ESPBTUUID char_uuid_;
std::tuple<Ts...> var_{};
uint16_t char_handle_{};
bool write_response_{false};
bool resolved_{false};
};
} // namespace esphome::ble_client
#endif // USE_BLE_CLIENT_GATT_NODES
@@ -1,43 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BLE_GATT_CLIENT
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cstdint>
namespace esphome::ble_client {
/// Reconnect backoff after repeated connect/discovery failures, shared by
/// both engines. 256 ms ticks in a uint16_t keep it 4 bytes; the ~4.7 h tick
/// wrap can at worst reinstate one stale hold-off of a minute.
class ConnectBackoff {
public:
bool holding_off() const {
return this->failures_ != 0 && static_cast<uint16_t>(now() - this->start_) < this->failures_ * STEP_TICKS;
}
void register_failure(const char *address_str) {
if (this->failures_ < MAX_STEPS)
this->failures_++;
this->start_ = now();
esph_log_w("ble_client", "[%s] Holding off reconnect for %u s", address_str, this->failures_ * 10u);
}
void reset() { this->failures_ = 0; }
private:
// ~10 s per consecutive failure, capped so a flapping peer retries within
// a minute at worst.
static constexpr uint16_t STEP_TICKS = 40; // x 256 ms
static constexpr uint8_t MAX_STEPS = 6;
static uint16_t now() { return static_cast<uint16_t>(millis() >> 8); }
uint16_t start_{0};
uint8_t failures_{0};
};
} // namespace esphome::ble_client
#endif // USE_BLE_GATT_CLIENT
@@ -1,48 +0,0 @@
#include "ble_gatt_client.h"
#ifdef USE_BLE_GATT_CLIENT
#include "esphome/core/log.h"
namespace esphome::ble_device_base {
ESPHOME_LOG_TAG(TAG, "ble_device_base");
const GattCharacteristic *find_characteristic(const GattServiceTable &table, const GattService &service,
const ESPBTUUID &uuid) {
// 32-bit range math: a corrupt first/count pair cannot wrap past the check.
uint32_t end = uint32_t(service.first_characteristic) + service.characteristic_count;
if (end > table.characteristic_count) {
ESP_LOGW(TAG, "characteristic range out of bounds");
return nullptr;
}
for (uint32_t i = service.first_characteristic; i < end; i++) {
if (table.characteristics[i].uuid == uuid)
return &table.characteristics[i];
}
return nullptr;
}
const GattDescriptor *find_descriptor(const GattServiceTable &table, const GattCharacteristic &characteristic,
const ESPBTUUID &uuid) {
uint32_t end = uint32_t(characteristic.first_descriptor) + characteristic.descriptor_count;
if (end > table.descriptor_count) {
// Corrupt range, not a missing descriptor.
ESP_LOGW(TAG, "descriptor range out of bounds");
return nullptr;
}
for (uint32_t i = characteristic.first_descriptor; i < end; i++) {
if (table.descriptors[i].uuid == uuid)
return &table.descriptors[i];
}
return nullptr;
}
uint16_t find_cccd(const GattServiceTable &table, const GattCharacteristic &characteristic) {
const GattDescriptor *desc = find_descriptor(table, characteristic, ESPBTUUID::from_uint16(CCCD_UUID));
return desc != nullptr ? desc->handle : 0;
}
} // namespace esphome::ble_device_base
#endif // USE_BLE_GATT_CLIENT
@@ -11,16 +11,13 @@
// interface. All listener calls are delivered on the ESPHome main loop;
// borrowed data pointers are valid only for the duration of the call.
//
// Error domain (plain int, forwarded to the API without translation, so the
// values are wire-frozen - API clients interpret them):
// Error domain (plain int, forwarded to the API without translation):
// 0 success
// 1..0x11 ATT error codes (Bluetooth spec) - reserved; a backend whose
// native error codes land in this window must remap them out
// 1..0x11 ATT error codes (Bluetooth spec; BTstack and Bluedroid agree)
// GATT_ERR_NOT_CONNECTED (-1) no connection to the peer (on esp32 a raw
// ESP_FAIL from the stack shares this value; both read as a
// failed, unusable connection on the client side)
// GATT_ERR_NO_MEMORY (-2) backend storage exhausted
// -1..-15 reserved for future contract sentinels
// anything else: platform stack error/status code, surfaced opaquely.
// Connection events carry HCI status/disconnect reason codes (same code
// space on every controller).
@@ -102,23 +99,9 @@ class GattClientListener {
// The BLEGattConnection op surface, asserted where the alias binds
// (bluetooth_connection_gatt_backend.h). Operations return 0 when accepted (completion arrives
// through the listener) or a synchronous error (busy, not connected, stack
// rejection); one operation may be outstanding at a time. A connection-state
// or discovery completion always arrives from the event loop, never
// synchronously from inside the op call - a synchronous terminal
// on_connection_state from within gatt_disconnect() would re-enter the
// consumer mid-teardown. A local-only completion may instead report inline,
// before the op returns: notify_characteristic (a subscription-list edit on
// the rp2 backend) and a write without response whose payload the stack took
// immediately. Consumers must therefore arm whatever state their
// on_write_result/on_notify_state reads before issuing the op, never after
// the call returns. Semantics beyond the signatures:
// rejection); one operation may be outstanding at a time. Semantics beyond
// the signatures:
// - connect: addr_type is a BLE_ADDR_TYPE_* constant (ble_device.h).
// Returning 0 means the request is accepted, not that the radio acted: the
// backend owns integration with its platform's scan/connect arbitration
// (Bluedroid parks the request for the tracker's promote loop, which owns
// scan-stop/coex/one-connect-at-a-time; the rp2 backend opens immediately
// and relies on sighting-gated consumers). Consumers must not assume
// connect timing.
// - gatt_disconnect: also cancels a connect in progress (named to coexist
// with a platform stack's own void disconnect() on one backend class).
// Nonzero means nothing to tear down and no completion will follow; an
@@ -160,41 +143,6 @@ concept BLEGattConnectionContract = requires(T conn, GattClientListener *listene
{ conn.set_connection_type(ConnectionType{}) } -> std::same_as<void>;
};
// ---- service table lookup helpers ----
//
// Neutral, bounds-checked walks over a materialized GattServiceTable for
// direct consumers that resolve a known device's handles by UUID (streaming
// consumers forward the raw database and never need these). Linear search:
// the table exists only between discovery and release_services(), for one
// small known device.
/// Client Characteristic Configuration descriptor UUID (Bluetooth spec).
static constexpr uint16_t CCCD_UUID = 0x2902;
// Characteristic property bits (the Bluetooth-spec declaration byte carried
// in GattCharacteristic::properties; the ESP-IDF macros for these do not
// exist on the other platforms).
static constexpr uint8_t GATT_CHAR_PROP_WRITE_NO_RSP = 0x04;
static constexpr uint8_t GATT_CHAR_PROP_WRITE = 0x08;
inline const GattService *find_service(const GattServiceTable &table, const ESPBTUUID &uuid) {
for (uint16_t i = 0; i < table.service_count; i++) {
if (table.services[i].uuid == uuid)
return &table.services[i];
}
return nullptr;
}
const GattCharacteristic *find_characteristic(const GattServiceTable &table, const GattService &service,
const ESPBTUUID &uuid);
const GattDescriptor *find_descriptor(const GattServiceTable &table, const GattCharacteristic &characteristic,
const ESPBTUUID &uuid);
/// Handle of the characteristic's Client Characteristic Configuration
/// descriptor (0x2902), or 0 when it has none.
uint16_t find_cccd(const GattServiceTable &table, const GattCharacteristic &characteristic);
} // namespace esphome::ble_device_base
#endif // USE_BLE_GATT_CLIENT
@@ -1,14 +1,12 @@
"""Per-platform GATT connection backends and the helpers to embed one.
Backends: esp32 Bluedroid, rp2 BTstack. No user-facing configuration; a
consumer's codegen declares and registers the backend instances — the
Bluetooth proxy through its per-slot connection wrappers (a streaming
consumer), and the neutral ble_client through gatt_client_schema() +
new_gatt_backend().
Backends: esp32 Bluedroid, rp2 BTstack. No user-facing configuration; the
Bluetooth proxy's codegen declares and registers the backend instances
through gatt_client_schema()/hub_connection_schema() + new_gatt_backend().
"""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from dataclasses import dataclass
import esphome.codegen as cg
from esphome.components import rp2040_ble
@@ -25,8 +23,7 @@ from esphome.types import ConfigType
def AUTO_LOAD() -> list[str]:
"""ble_device_base plus the platform BLE stack the build's backend
registers with (the Bluedroid header includes the tracker's), so
consumers stay platform-blind. The platform-less arm serves tooling that
resolves the manifest without a target."""
consumers need not know. The platform-less arm serves manifest tooling."""
if CORE.is_esp32:
return ["ble_device_base", "esp32_ble_tracker"]
if CORE.is_rp2:
@@ -66,8 +63,6 @@ DOMAIN = "bluetooth_connection"
@dataclass
class _ConnectionData:
rp2_backend_count: int = 0
# GATT connection slots claimed this run, for the platform cap check.
slot_consumers: list[str] = field(default_factory=list)
def _get_data() -> _ConnectionData:
@@ -124,10 +119,6 @@ class _PlatformBackend:
backend_class: cg.MockObjClass
schema_fragment: Callable[[], cv.Schema]
register: Callable[[cg.MockObj, ConfigType], Awaitable[None]]
# Selects the backend's alias-ladder arm (order-independent arms).
define: str
# The backend's on-demand materializer gate, when it has one.
materializer_define: str | None = None
# The single registry of platforms with a GATT client backend; a platform
@@ -135,20 +126,11 @@ class _PlatformBackend:
# platform's arm.
_PLATFORM_BACKENDS: dict[str, _PlatformBackend] = {
PLATFORM_ESP32: _PlatformBackend(
BluedroidGattClient,
_esp32_schema_fragment,
_esp32_register,
"USE_BLE_GATT_BACKEND_BLUEDROID",
materializer_define="USE_BLUEDROID_GATT_SERVICE_TABLE",
),
PLATFORM_RP2: _PlatformBackend(
RP2GattClient, _rp2_schema_fragment, _rp2_register, "USE_BLE_GATT_BACKEND_RP2"
BluedroidGattClient, _esp32_schema_fragment, _esp32_register
),
PLATFORM_RP2: _PlatformBackend(RP2GattClient, _rp2_schema_fragment, _rp2_register),
}
# Gates dedicated-backend consumers (cv.only_on).
GATT_CLIENT_PLATFORMS = list(_PLATFORM_BACKENDS)
def _backend_entry(platform: str | None = None) -> _PlatformBackend:
key = platform if platform is not None else CORE.target_platform
@@ -183,97 +165,21 @@ def hub_connection_schema(platform: str | None = None) -> cv.Schema:
)
def _charge_esp32_budget(count: int, consumer: str, config: ConfigType) -> None:
from esphome.components import esp32_ble
esp32_ble.consume_connection_slots(count, consumer)(config)
def _charge_rp2_budget(count: int, consumer: str, config: ConfigType) -> None:
rp2040_ble.consume_connection_slots(count, consumer)(config)
# Platforms whose BLE stack owns its own connection budget: the claim is
# charged there and that stack's final validation is the one place an
# overcommit is reported (never two messages for one misconfiguration). One
# mapping drives both, so a platform can never be skipped without a charge.
_STACK_BUDGETS: dict[str, Callable[[int, str, ConfigType], None]] = {
PLATFORM_ESP32: _charge_esp32_budget,
PLATFORM_RP2: _charge_rp2_budget,
}
def consume_gatt_slot(
consumer: str, count: int = 1
) -> Callable[[ConfigType], ConfigType]:
"""Validator claiming GATT connection slots - the one spelling for every
claimant. The neutral ledger covers backend platforms without a stack
budget (the cap check in FINAL_VALIDATE_SCHEMA)."""
def validator(config: ConfigType) -> ConfigType:
_get_data().slot_consumers.extend([consumer] * count)
if (charge := _STACK_BUDGETS.get(CORE.target_platform)) is not None:
charge(count, consumer, config)
return config
return validator
def _validate_slot_totals(config: ConfigType) -> ConfigType:
# Skipped in testing mode so grouped component builds can co-exist
# (mirrors esp32_ble.validate_connection_slots).
if CORE.testing_mode:
return config
if CORE.target_platform in _STACK_BUDGETS:
return config
if (cap := HUB_MAX_CONNECTIONS.get(CORE.target_platform)) is None:
# Any backend platform without a stack budget must carry a cap here
# or fail loudly, never fail open.
if CORE.target_platform in _PLATFORM_BACKENDS:
raise cv.Invalid(
f"{CORE.target_platform} has a GATT backend but no slot cap "
"in HUB_MAX_CONNECTIONS"
)
return config
claimed = _get_data().slot_consumers
if len(claimed) > cap:
raise cv.Invalid(
f"{CORE.target_platform} supports at most {cap} GATT client "
f"connection(s); {len(claimed)} requested by: {', '.join(claimed)}"
)
return config
FINAL_VALIDATE_SCHEMA = _validate_slot_totals
async def new_gatt_backend(
config: ConfigType, *, service_table: bool = True
) -> cg.MockObj:
async def new_gatt_backend(config: ConfigType) -> cg.MockObj:
"""Instantiate the backend declared by gatt_client_schema() and register
it with its platform stack. The connection slot is claimed at validation
(the consume_gatt_slot validators), not here.
service_table is honored by the Bluedroid backend only: forward
scaffolding for the first esp32 direct consumer, load-bearing on no
current build (rp2 ignores the define and always materializes - its
proxy hub streams through get_service_table(), so it must keep the
materializer regardless of the flag).
(the proxy's slot validators), not here.
"""
from esphome.components import ble_device_base
entry = _backend_entry()
ble_device_base.request_gatt_client()
cg.add_define(entry.define)
if service_table and entry.materializer_define is not None:
cg.add_define(entry.materializer_define)
backend = cg.new_Pvariable(config[CONF_BACKEND_ID])
# The backend is the slot's real Component: component keys from the
# connection entry (setup_priority, ...) apply to it. Consumers whose own
# schema carries keys that register_component would misapply to the
# backend (e.g. a polling interval) must not put them in this config.
await cg.register_component(backend, config)
await entry.register(backend, config)
await _backend_entry().register(backend, config)
return backend
@@ -281,7 +187,6 @@ async def new_gatt_backend(
# list (this module cannot import bluetooth_proxy to derive it).
SOURCE_FILE_FRAMEWORKS: dict[str, set[PlatformFramework]] = {
"bluetooth_connection_bluedroid.cpp": frameworks_for_platforms([PLATFORM_ESP32]),
"gatt_service_table_bluedroid.cpp": frameworks_for_platforms([PLATFORM_ESP32]),
# Every hub platform the proxy admits (the file compiles empty where
# USE_BLE_GATT_CLIENT is not defined), so a platform gaining a backend
# cannot hit a missing-symbol trap here.
@@ -46,7 +46,7 @@ BatchClose close_service_batch(api::BluetoothGATTGetServicesResponse &resp, size
#endif // USE_BLUETOOTH_PROXY_CONNECTIONS
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT)
#if defined(USE_ESP32) && defined(USE_BLE_GATT_CLIENT)
namespace esphome::bluetooth_connection {
// Address-scoped Bluedroid maintenance. Gated with the connection surface:
@@ -65,4 +65,4 @@ conn_err_t clear_gatt_cache(uint64_t address) {
}
} // namespace esphome::bluetooth_connection
#endif // USE_ESP32_BLE && USE_BLE_GATT_CLIENT
#endif // USE_ESP32 && USE_BLE_GATT_CLIENT
@@ -49,16 +49,15 @@ static constexpr conn_err_t CONN_OK = 0;
// GATT contract so backend and wrapper cannot drift.
static constexpr conn_err_t GATT_NOT_CONNECTED = ble_device_base::GATT_ERR_NOT_CONNECTED;
// What the build's connection backend supports beyond GATT operations; the
// proxy derives its feature flags and legacy version from these. Keyed on
// the backend define, never the platform, so a second backend on one
// platform carries its own facts.
#if defined(USE_BLE_GATT_BACKEND_BLUEDROID)
// What the platform's connection backend supports beyond GATT operations;
// the proxy derives its feature flags and legacy version from these.
#if defined(USE_ESP32)
static constexpr bool SUPPORTS_PAIRING = true;
static constexpr bool SUPPORTS_CACHE_CLEARING = true;
#elif defined(USE_BLE_GATT_BACKEND_RP2)
#elif defined(USE_RP2040_BLE) && defined(USE_BLE_GATT_CLIENT)
// The rp2 BTstack backend pairs (just works + bonding); it has no service
// cache to clear.
// cache to clear. Keyed on the backend, not the generic client define, so a
// future backend without pairing keeps the stub arm below.
static constexpr bool SUPPORTS_PAIRING = true;
static constexpr bool SUPPORTS_CACHE_CLEARING = false;
#else
@@ -66,14 +65,13 @@ static constexpr bool SUPPORTS_PAIRING = false;
static constexpr bool SUPPORTS_CACHE_CLEARING = false;
#endif
// Address-scoped (not connection-scoped) maintenance requests; keyed on the
// stack (the calls need no backend instance).
#if (defined(USE_ESP32_BLE) || defined(USE_RP2040_BLE)) && defined(USE_BLE_GATT_CLIENT)
// Address-scoped (not connection-scoped) maintenance requests.
#if (defined(USE_ESP32) || defined(USE_RP2040_BLE)) && defined(USE_BLE_GATT_CLIENT)
conn_err_t unpair_device(uint64_t address);
#else
inline conn_err_t unpair_device(uint64_t) { return GATT_NOT_CONNECTED; }
#endif
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT)
#if defined(USE_ESP32) && defined(USE_BLE_GATT_CLIENT)
conn_err_t clear_gatt_cache(uint64_t address);
#else
inline conn_err_t clear_gatt_cache(uint64_t) { return GATT_NOT_CONNECTED; }
@@ -2,11 +2,10 @@
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT)
#include "bluetooth_connection.h"
// The in-place streamer serves the proxy's service-discovery API; backend-only
// builds compile without the proxy headers or the streamer.
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
#include "bluetooth_connection.h"
#include "bluetooth_connection_hub.h"
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
@@ -346,9 +345,6 @@ int BluedroidGattClient::update_connection_params(uint16_t min_interval, uint16_
void BluedroidGattClient::release_services() {
this->service_total_ = 0;
#ifdef USE_BLUEDROID_GATT_SERVICE_TABLE
this->table_.free();
#endif
// Always set: terminates any in-flight stream on every cache config.
this->services_released_ = true;
#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
@@ -361,24 +357,6 @@ void BluedroidGattClient::release_services() {
#endif
}
#ifdef USE_BLUEDROID_GATT_SERVICE_TABLE
ble_device_base::GattServiceTable BluedroidGattClient::get_service_table() {
// Lifetime: every teardown path (CLOSE_EVT, the safety timeout, stack-down,
// passive DISCONNECT) routes through release_services(), so a materialized
// table cannot outlive its link.
if (this->table_.empty() &&
(this->services_released_ || this->service_total_ == 0 ||
!this->table_.build(this->gattc_if_, this->conn_id_, this->service_total_, this->connection_index_))) {
// Released / no services / failed build all collapse to empty; the
// build failures warned above, log the quiet two.
ESP_LOGD(TAG, "[%d] No service table (released=%d, services=%u)", this->connection_index_, this->services_released_,
this->service_total_);
return {};
}
return this->table_.view();
}
#endif // USE_BLUEDROID_GATT_SERVICE_TABLE
// ---- internals ----
bool BluedroidGattClient::check_addr_(const esp_bd_addr_t &addr) const {
@@ -425,11 +403,6 @@ void BluedroidGattClient::log_gattc_warning_(const char *operation, int code) {
// ---- service streaming ----
int BluedroidGattClient::handle_search_cmpl_(esp_gatt_status_t status) {
#ifdef USE_BLUEDROID_GATT_SERVICE_TABLE
// Re-discovery moves the counts the table view derives offsets from; free
// the stale table.
this->table_.free();
#endif
// Step down from the fast discovery params.
this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
if (status != ESP_GATT_OK) {
@@ -11,9 +11,6 @@
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT)
#include "bluetooth_connection.h"
#include "gatt_service_table_bluedroid.h"
#include "esphome/components/ble_device_base/ble_gatt_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/core/component.h"
@@ -79,16 +76,11 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
int notify_characteristic(uint16_t handle, bool enable);
int pair();
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout);
// On-demand table for direct consumers; the proxy streams instead, so the
// materializer compiles only under USE_BLUEDROID_GATT_SERVICE_TABLE (emitted by
// direct-consumer codegen, never by the proxy).
#ifdef USE_BLUEDROID_GATT_SERVICE_TABLE
ble_device_base::GattServiceTable get_service_table();
#else
// A direct consumer reaching this stub misconfigured its codegen
// (service_table=False): the empty table reads as a service-less peer.
// Contract stub: the proxy streams in place; the on-demand materializer
// for direct consumers lands with #18205. NOTE: a direct consumer reaching
// this stub gets an empty table indistinguishable from a service-less
// peer - do not ship one against this backend before the materializer.
ble_device_base::GattServiceTable get_service_table() { return {}; }
#endif
void release_services();
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
@@ -118,9 +110,6 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
// Group 1: pointers / composed objects
ble_device_base::GattClientListener *listener_{nullptr};
#ifdef USE_BLUEDROID_GATT_SERVICE_TABLE
BluedroidServiceTable table_;
#endif
// Group 2: 4-byte types
uint32_t disconnecting_started_{0};
@@ -12,12 +12,10 @@
#include "esphome/components/ble_device_base/ble_gatt_client.h"
// Arms are keyed on codegen-emitted per-backend defines (_PLATFORM_BACKENDS
// in __init__.py), so they are order-independent.
#if defined(USE_BLE_GATT_BACKEND_RP2)
#if defined(USE_RP2040_BLE)
#include "bluetooth_connection_rp2.h"
#define ESPHOME_BLE_GATT_CONNECTION_TYPE bluetooth_connection::RP2GattClient
#elif defined(USE_BLE_GATT_BACKEND_BLUEDROID)
#elif defined(USE_ESP32_BLE)
#include "bluetooth_connection_bluedroid.h"
#define ESPHOME_BLE_GATT_CONNECTION_TYPE bluetooth_connection::BluedroidGattClient
#elif defined(USE_BLE_GATT_CLIENT_STUB_BACKEND)
@@ -1,198 +0,0 @@
#include "gatt_service_table_bluedroid.h"
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT) && defined(USE_BLUEDROID_GATT_SERVICE_TABLE)
#include "esphome/core/log.h"
namespace esphome::bluetooth_connection {
ESPHOME_LOG_TAG(TAG, "bluetooth_connection");
// A stack that never reports end-of-range would otherwise walk forever.
static constexpr uint16_t MAX_DESCRIPTORS_PER_CHARACTERISTIC = 64;
// Shared enumeration for both build passes: an identical walk order is what
// lets the counting pass size the block the filling pass fills.
// INVALID_OFFSET/NOT_FOUND mean end-of-range; anything else is a failure.
template<typename ServiceFn, typename CharFn, typename DescFn>
bool BluedroidServiceTable::walk_(ServiceFn &&on_service, CharFn &&on_char, DescFn &&on_desc) {
for (uint16_t s = 0; s < this->service_total_; s++) {
esp_gattc_service_elem_t svc;
uint16_t svc_count = 1;
auto svc_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, &svc, &svc_count, s);
if (svc_status != ESP_GATT_OK || svc_count == 0) {
this->log_walk_warning_("esp_ble_gattc_get_service", svc_status);
return false;
}
if (!on_service(s, svc)) {
return false;
}
uint16_t svc_chars = 0;
auto count_status = esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
svc.start_handle, svc.end_handle, 0, &svc_chars);
if (count_status != ESP_GATT_OK) {
this->log_walk_warning_("esp_ble_gattc_get_attr_count", count_status);
return false;
}
for (uint16_t c = 0; c < svc_chars; c++) {
esp_gattc_char_elem_t chr;
uint16_t char_count = 1;
auto status = esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, svc.start_handle, svc.end_handle, &chr,
&char_count, c);
if (status != ESP_GATT_OK || char_count == 0) {
// An early terminator contradicts svc_chars from the same cache;
// never build a silently truncated table.
this->log_walk_warning_("esp_ble_gattc_get_all_char", status);
return false;
}
if (!on_char(svc, chr)) {
return false;
}
for (uint16_t d = 0;; d++) {
if (d == MAX_DESCRIPTORS_PER_CHARACTERISTIC) {
// A stack that never reports end-of-range; fail like every other
// inconsistency instead of truncating the table silently.
ESP_LOGW(TAG, "[%d] Descriptor walk exceeded %u entries", this->log_index_,
MAX_DESCRIPTORS_PER_CHARACTERISTIC);
return false;
}
esp_gattc_descr_elem_t desc;
uint16_t desc_count = 1;
auto desc_status =
esp_ble_gattc_get_all_descr(this->gattc_if_, this->conn_id_, chr.char_handle, &desc, &desc_count, d);
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
break;
}
if (desc_status != ESP_GATT_OK || desc_count == 0) {
this->log_walk_warning_("esp_ble_gattc_get_all_descr", desc_status);
return false;
}
if (!on_desc(chr, desc)) {
return false;
}
}
}
}
return true;
}
bool BluedroidServiceTable::count_services(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t *total) {
uint16_t primary = 0;
uint16_t secondary = 0;
if (esp_ble_gattc_get_attr_count(gattc_if, conn_id, ESP_GATT_DB_PRIMARY_SERVICE, 0x0001, 0xFFFF, 0, &primary) !=
ESP_GATT_OK ||
esp_ble_gattc_get_attr_count(gattc_if, conn_id, ESP_GATT_DB_SECONDARY_SERVICE, 0x0001, 0xFFFF, 0, &secondary) !=
ESP_GATT_OK) {
// A failed count must not read as an authoritative empty database.
return false;
}
*total = primary + secondary;
return true;
}
bool BluedroidServiceTable::build(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t service_total, uint8_t log_index) {
this->free();
this->gattc_if_ = gattc_if;
this->conn_id_ = conn_id;
this->service_total_ = service_total;
this->log_index_ = log_index;
// Pass 1: count, so one exact-size block holds the whole table.
uint16_t char_total = 0;
uint16_t desc_total = 0;
bool counted = this->walk_([](uint16_t, const esp_gattc_service_elem_t &) { return true; },
[&](const esp_gattc_service_elem_t &, const esp_gattc_char_elem_t &) {
char_total++;
return true;
},
[&](const esp_gattc_char_elem_t &, const esp_gattc_descr_elem_t &) {
desc_total++;
return true;
});
if (!counted) {
ESP_LOGW(TAG, "[%d] Service table walk failed during count", this->log_index_);
this->free();
return false;
}
// The arrays share one block; carving stays aligned because each struct's
// strictest member is the UUID and array sizes are multiples of it.
static_assert(alignof(ble_device_base::GattService) >= alignof(ble_device_base::GattCharacteristic) &&
alignof(ble_device_base::GattCharacteristic) >= alignof(ble_device_base::GattDescriptor));
size_t svc_bytes = this->service_total_ * sizeof(ble_device_base::GattService);
size_t char_bytes = char_total * sizeof(ble_device_base::GattCharacteristic);
size_t total_bytes = svc_bytes + char_bytes + desc_total * sizeof(ble_device_base::GattDescriptor);
RAMAllocator<uint8_t> allocator(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
this->storage_ = allocator.allocate(total_bytes);
if (this->storage_ == nullptr) {
ESP_LOGW(TAG, "[%d] Service table allocation failed (%u bytes)", this->log_index_,
static_cast<unsigned>(total_bytes));
this->free();
return false;
}
auto *services = reinterpret_cast<ble_device_base::GattService *>(this->storage_);
auto *characteristics = reinterpret_cast<ble_device_base::GattCharacteristic *>(this->storage_ + svc_bytes);
auto *descriptors = reinterpret_cast<ble_device_base::GattDescriptor *>(this->storage_ + svc_bytes + char_bytes);
// Pass 2: fill, bounded by the pass-1 totals. A bound trip or a shortfall
// means the cached database changed between the passes; fail the build
// rather than serve an inconsistent table (the consumer retries).
uint16_t char_index = 0;
uint16_t desc_index = 0;
ble_device_base::GattService *cur_service = nullptr;
ble_device_base::GattCharacteristic *cur_char = nullptr;
bool filled = this->walk_(
[&](uint16_t s, const esp_gattc_service_elem_t &svc) {
cur_service = &services[s];
cur_service->uuid = ble_device_base::ESPBTUUID::from_uuid(svc.uuid);
cur_service->start_handle = svc.start_handle;
cur_service->end_handle = svc.end_handle;
cur_service->first_characteristic = char_index;
cur_service->characteristic_count = 0;
return true;
},
[&](const esp_gattc_service_elem_t &svc, const esp_gattc_char_elem_t &chr) {
if (char_index >= char_total) {
return false;
}
cur_char = &characteristics[char_index++];
cur_char->uuid = ble_device_base::ESPBTUUID::from_uuid(chr.uuid);
cur_char->value_handle = chr.char_handle;
// Bluedroid addresses descriptors by characteristic handle, so the
// table's end_handle only needs the service-bounded upper bound.
cur_char->end_handle = svc.end_handle;
cur_char->properties = chr.properties;
cur_char->first_descriptor = desc_index;
cur_char->descriptor_count = 0;
cur_service->characteristic_count++;
return true;
},
[&](const esp_gattc_char_elem_t &, const esp_gattc_descr_elem_t &desc) {
if (desc_index >= desc_total) {
return false;
}
descriptors[desc_index].uuid = ble_device_base::ESPBTUUID::from_uuid(desc.uuid);
descriptors[desc_index].handle = desc.handle;
desc_index++;
cur_char->descriptor_count++;
return true;
});
if (!filled || char_index != char_total || desc_index != desc_total) {
// Walk error or the database changed between passes; better an empty
// table than a corrupt one.
ESP_LOGW(TAG, "[%d] Service table walk mismatch, discarding", this->log_index_);
this->free();
return false;
}
this->char_total_ = char_total;
this->desc_total_ = desc_total;
return true;
}
void BluedroidServiceTable::log_walk_warning_(const char *operation, int code) {
ESP_LOGW(TAG, "[%d] %s failed, status=%d", this->log_index_, operation, code);
}
} // namespace esphome::bluetooth_connection
#endif // USE_ESP32_BLE && USE_BLE_GATT_CLIENT && USE_BLUEDROID_GATT_SERVICE_TABLE
@@ -1,82 +0,0 @@
// Owning two-pass materializer of one Bluedroid GATT database snapshot into
// the neutral GattServiceTable layout, shared by the BluedroidGattClient
// backend and ble_client's esp32 engine.
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT) && defined(USE_BLUEDROID_GATT_SERVICE_TABLE)
#include "esphome/components/ble_device_base/ble_gatt_client.h"
#include "esphome/core/helpers.h"
#include <esp_gattc_api.h>
namespace esphome::bluetooth_connection {
class BluedroidServiceTable {
public:
~BluedroidServiceTable() { this->free(); }
// Owns storage_; a copy would double-free. The default constructor is user
// provided, not "= default", so value-initializing the enclosing backend
// cannot zero-fill .bss that is already zero.
BluedroidServiceTable() {}
BluedroidServiceTable(const BluedroidServiceTable &) = delete;
BluedroidServiceTable &operator=(const BluedroidServiceTable &) = delete;
/// The service count build() requires: the stack's PRIMARY+SECONDARY
/// attribute totals, never the SEARCH_RES event count.
static bool count_services(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t *total);
/// Two-pass build from the stack's cached database (service_total from
/// count_services()). log_index labels warnings. Frees any previous table
/// first; on failure the table is left empty.
bool build(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t service_total, uint8_t log_index);
// The view is carved from the storage block and the counts on each call
// (a cold path) rather than cached, saving a per-instance table member.
ble_device_base::GattServiceTable view() const {
size_t svc_bytes = this->service_total_ * sizeof(ble_device_base::GattService);
size_t char_bytes = this->char_total_ * sizeof(ble_device_base::GattCharacteristic);
return {reinterpret_cast<const ble_device_base::GattService *>(this->storage_),
reinterpret_cast<const ble_device_base::GattCharacteristic *>(this->storage_ + svc_bytes),
reinterpret_cast<const ble_device_base::GattDescriptor *>(this->storage_ + svc_bytes + char_bytes),
this->service_total_,
this->char_total_,
this->desc_total_};
}
// Always resets the counts: a failed build must never leave a non-zero
// service_total_ behind a null table.
void free() {
if (this->storage_ != nullptr) {
RAMAllocator<uint8_t> allocator(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
allocator.deallocate(this->storage_, 0);
this->storage_ = nullptr;
}
this->service_total_ = 0;
this->char_total_ = 0;
this->desc_total_ = 0;
}
bool empty() const { return this->storage_ == nullptr; }
private:
template<typename ServiceFn, typename CharFn, typename DescFn>
bool walk_(ServiceFn &&on_service, CharFn &&on_char, DescFn &&on_desc);
void log_walk_warning_(const char *operation, int code);
uint8_t *storage_{nullptr};
uint16_t service_total_{0};
uint16_t char_total_{0};
uint16_t desc_total_{0};
// Walk context, set by build().
uint16_t conn_id_{0};
esp_gatt_if_t gattc_if_{}; // uint8_t width
uint8_t log_index_{0};
};
} // namespace esphome::bluetooth_connection
#endif // USE_ESP32_BLE && USE_BLE_GATT_CLIENT && USE_BLUEDROID_GATT_SERVICE_TABLE
+6 -18
View File
@@ -94,20 +94,10 @@ def _esp32_config_schema() -> cv.All:
CONNECTION_SCHEMA = bluetooth_connection.hub_connection_schema(PLATFORM_ESP32)
def validate_connections(config: ConfigType) -> ConfigType:
if CONF_CONNECTIONS in config:
# Explicit entries claim slots like the generated ones; dev
# historically skipped this, letting an explicit-connections
# config evade the controller budget.
bluetooth_connection.consume_gatt_slot(
"bluetooth_proxy", len(config[CONF_CONNECTIONS])
)(config)
return config
if not config[CONF_ACTIVE]:
if CONF_CONNECTIONS in config or not config[CONF_ACTIVE]:
return config
connection_slots: int = config[CONF_CONNECTION_SLOTS]
bluetooth_connection.consume_gatt_slot("bluetooth_proxy", connection_slots)(
config
)
esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config)
return {
**config,
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
@@ -159,14 +149,14 @@ def _rp2_config_schema() -> cv.All:
connection_schema = bluetooth_connection.hub_connection_schema(PLATFORM_RP2)
def populate_connections(config: ConfigType) -> ConfigType:
from esphome.components import rp2040_ble
# One wrapper + backend pair per slot, declared during validation so
# their ids exist for codegen (the esp32 arm's `connections` pattern).
if not config[CONF_ACTIVE]:
return config
connection_slots: int = config[CONF_CONNECTION_SLOTS]
bluetooth_connection.consume_gatt_slot("bluetooth_proxy", connection_slots)(
config
)
rp2040_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config)
return {
**config,
CONF_CONNECTIONS: [connection_schema({}) for _ in range(connection_slots)],
@@ -216,9 +206,7 @@ async def _connections_to_code(var: cg.MockObj, config: ConfigType) -> None:
# sends those requests and their handlers and encoders are dead.
cg.add_define("USE_BLUETOOTH_PROXY_CONNECTIONS")
for connection_conf in connections:
backend = await bluetooth_connection.new_gatt_backend(
connection_conf, service_table=False
)
backend = await bluetooth_connection.new_gatt_backend(connection_conf)
connection = cg.new_Pvariable(connection_conf[CONF_ID])
cg.add(connection.set_backend(backend))
cg.add(var.register_connection(connection))
@@ -1,7 +1,7 @@
#include "bme680_bsec.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <string>
#include "esphome/core/progmem.h"
namespace esphome::bme680_bsec {
#ifdef USE_BSEC
@@ -9,7 +9,7 @@ ESPHOME_LOG_TAG(TAG, "bme680_bsec.sensor");
static constexpr uint32_t READ_TIMEOUT_ID = 0;
static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
PROGMEM_STRING_TABLE(IaqAccuracyStrings, "Stabilizing", "Uncertain", "Calibrating", "Calibrated");
std::vector<BME680BSECComponent *>
BME680BSECComponent::instances; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -386,10 +386,11 @@ void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_out
switch (outputs[i].sensor_id) {
case BSEC_OUTPUT_IAQ:
case BSEC_OUTPUT_STATIC_IAQ: {
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, std::size(IAQ_ACCURACY_STATES) - 1);
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, IaqAccuracyStrings::LAST_INDEX);
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
this->queue_push_([this, accuracy]() {
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
this->publish_sensor_(this->iaq_accuracy_text_sensor_,
IaqAccuracyStrings::get_progmem_str(accuracy, IaqAccuracyStrings::LAST_INDEX));
});
this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
@@ -435,8 +436,9 @@ void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, b
sensor->publish_state(value);
}
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) {
if (!sensor || (sensor->has_state() && sensor->state == value)) {
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, ProgmemStr value) {
if (!sensor ||
(sensor->has_state() && ESPHOME_strcmp_P(sensor->state.c_str(), reinterpret_cast<ESPHOME_PGM_P>(value)) == 0)) {
return;
}
sensor->publish_state(value);
+1 -1
View File
@@ -77,7 +77,7 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
int64_t get_time_ns_();
void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
void publish_sensor_(text_sensor::TextSensor *sensor, ProgmemStr value);
void snapshot_state_(); // Fetch the current BSEC library state and save it in the bsec_state_data_ member (volatile
// memory)
+7 -4
View File
@@ -2,6 +2,7 @@
#include "cc1101pa.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cmath>
#include <cstring>
@@ -14,6 +15,9 @@ namespace esphome::cc1101 {
ESPHOME_LOG_TAG(TAG, "cc1101");
static constexpr size_t TX_FIFO_SIZE = 64;
// MDMCFG2 MOD_FORMAT values
PROGMEM_STRING_TABLE(ModulationStrings, "2-FSK", "GFSK", "UNUSED", "ASK/OOK", "4-FSK", "UNUSED", "UNUSED", "MSK");
static void split_float(float value, int mbits, uint8_t &e, uint32_t &m) {
int e_tmp;
float m_tmp = std::frexp(value, &e_tmp);
@@ -238,8 +242,6 @@ void CC1101Component::loop() {
}
void CC1101Component::dump_config() {
static const char *const MODULATION_NAMES[] = {"2-FSK", "GFSK", "UNUSED", "ASK/OOK",
"4-FSK", "UNUSED", "UNUSED", "MSK"};
int32_t freq = static_cast<int32_t>(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) *
XTAL_FREQUENCY / (1 << 16);
float symbol_rate = (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
@@ -253,8 +255,9 @@ void CC1101Component::dump_config() {
" Symbol Rate: %.0f baud\n"
" Filter Bandwidth: %.1f Hz\n"
" Output Power: %.1f dBm",
this->chip_id_, freq, this->state_.CHANNR, MODULATION_NAMES[this->state_.MOD_FORMAT & 0x07],
symbol_rate, bw, this->output_power_effective_);
this->chip_id_, freq, this->state_.CHANNR,
LOG_STR_ARG(ModulationStrings::get_log_str(this->state_.MOD_FORMAT & 0x07, 0)), symbol_rate, bw,
this->output_power_effective_);
LOG_PIN(" CS Pin: ", this->cs_);
}
@@ -6,12 +6,7 @@ ESPHOME_LOG_TAG(TAG, "cst226.touchscreen");
void CST226Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 5});
this->set_timeout(30, [this] { this->continue_setup_(); });
} else {
this->continue_setup_();
@@ -43,9 +43,7 @@ void CST328Touchscreen::setup() {
}
void CST328Touchscreen::reset_device_() {
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
pulse_reset(this->reset_pin_, {.low_ms = 5});
this->set_timeout(CST328_TRANSITION_TIMEOUT, [this] { this->continue_setup_(); });
}
@@ -52,12 +52,7 @@ void CST816Touchscreen::continue_setup_() {
void CST816Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 5});
this->set_timeout(30, [this] { this->continue_setup_(); });
} else {
this->continue_setup_();
@@ -10,14 +10,7 @@ static constexpr uint32_t REREAD_TIMEOUT_ID = 1;
static constexpr uint32_t REREAD_DELAY_MS = 20;
void CST9220Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(10);
this->reset_pin_->digital_write(true);
}
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 10});
// Wait for the controller to leave its bootloader before talking to it.
this->set_timeout(30, [this] { this->continue_setup_(); });
}
@@ -42,6 +42,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_LINE_READER")
await uart.new_uart_component(config)
@@ -11,10 +11,10 @@
namespace esphome::dfrobot_sen0395 {
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0395");
static constexpr char ASCII_CR = 0x0D;
static constexpr char ASCII_LF = 0x0A;
static constexpr float DET_RANGE_STEP_M = 0.15f;
static constexpr float LATENCY_STEP_S = 0.025f;
// Sent after a reset with no line end
static constexpr char PROMPT[] PROGMEM = "leapMMW:/>";
// What each CommandType cannot do while the sensor runs; empty for commands that do not need it stopped
PROGMEM_STRING_TABLE(BusyActions, "", "configure range config", "configure output latency",
@@ -121,9 +121,9 @@ bool DfrobotSen0395Component::run_command_(const Command &cmd, uint32_t now) {
return false; // Command not done yet
}
if (this->read_message_()) {
if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("is not recognized as a CLI command")) == nullptr) {
if (!this->handle_reply_(cmd))
if (const char *message = this->read_message_()) {
if (ESPHOME_strstr_P(message, ESPHOME_PSTR("is not recognized as a CLI command")) == nullptr) {
if (!this->handle_reply_(cmd, message))
return false; // Command not done yet
this->read_message_(); // Skip the prompt
return true;
@@ -146,12 +146,13 @@ bool DfrobotSen0395Component::run_command_(const Command &cmd, uint32_t now) {
}
void DfrobotSen0395Component::read_state_() {
if (!this->read_message_())
const char *message = this->read_message_();
if (message == nullptr)
return;
if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("$JYBSS,0, , , *")) != nullptr) {
if (ESPHOME_strstr_P(message, ESPHOME_PSTR("$JYBSS,0, , , *")) != nullptr) {
this->set_detected_(false);
this->set_active(true);
} else if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("$JYBSS,1, , , *")) != nullptr) {
} else if (ESPHOME_strstr_P(message, ESPHOME_PSTR("$JYBSS,1, , , *")) != nullptr) {
this->set_detected_(true);
this->set_active(true);
}
@@ -206,8 +207,7 @@ void DfrobotSen0395Component::format_command(const Command &cmd, char *buf, size
}
}
bool DfrobotSen0395Component::handle_reply_(const Command &cmd) {
const char *message = this->read_buffer_;
bool DfrobotSen0395Component::handle_reply_(const Command &cmd, const char *message) {
auto is = [message](ESPHOME_PGM_P text) { return ESPHOME_strcmp_P(message, text) == 0; };
// Commands with their own replies
@@ -230,7 +230,7 @@ bool DfrobotSen0395Component::handle_reply_(const Command &cmd) {
}
return true;
case CommandType::COMMAND_TYPE_RESET_SYSTEM:
if (!is(ESPHOME_PSTR("leapMMW:/>")))
if (!is(PROMPT))
return false;
ESP_LOGI(TAG, "Restarted sensor.");
return true;
@@ -300,34 +300,24 @@ bool DfrobotSen0395Component::handle_reply_(const Command &cmd) {
return true;
}
bool DfrobotSen0395Component::read_message_() {
const char *DfrobotSen0395Component::read_message_() {
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
if (this->read_pos_ == MMWAVE_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte);
if (byte == ASCII_CR)
continue;
if (byte >= 0x7F)
byte = '?'; // needs to be valid utf8 string for log functions.
this->read_buffer_[this->read_pos_] = byte;
if (this->read_pos_ == 9 && byte == '>')
this->read_buffer_[++this->read_pos_] = ASCII_LF;
if (this->read_buffer_[this->read_pos_] == ASCII_LF) {
this->read_buffer_[this->read_pos_] = 0;
this->read_pos_ = 0;
ESP_LOGV(TAG, "Message: %s", this->read_buffer_);
return true; // Full message in buffer
const char *line = this->line_reader_.feed(byte);
if (line == nullptr && byte == '>' && this->line_reader_.length() == sizeof(PROMPT) - 1) {
line = this->line_reader_.partial();
this->line_reader_.reset();
}
this->read_pos_++;
if (line != nullptr) {
ESP_LOGV(TAG, "Message: %s", line);
return line;
}
this->line_reader_.log_if_overflowed(TAG);
}
return false; // No full message yet
return nullptr;
}
void DfrobotSen0395Component::set_detected_(bool detected) {
@@ -4,6 +4,7 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/line_reader.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
@@ -14,7 +15,7 @@
namespace esphome::dfrobot_sen0395 {
static constexpr uint8_t MMWAVE_READ_BUFFER_LENGTH = 255;
static constexpr uint8_t MMWAVE_READ_BUFFER_LENGTH = 253; // largest LineReader size with a one byte position
static constexpr uint8_t COMMAND_QUEUE_SIZE = 20;
// Keep in sync with the BusyActions table in dfrobot_sen0395.cpp
@@ -121,8 +122,7 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
bool led_active_{false};
bool uart_presence_active_{false};
bool start_after_boot_{false};
char read_buffer_[MMWAVE_READ_BUFFER_LENGTH];
size_t read_pos_{0};
LineReader<MMWAVE_READ_BUFFER_LENGTH> line_reader_;
bool cmd_sent_{false};
uint8_t retries_left_{2};
StaticRingBuffer<Command, COMMAND_QUEUE_SIZE> cmd_queue_;
@@ -131,12 +131,13 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
int16_t det_range_steps_[8]{};
uint16_t latency_steps_[2]{};
bool read_message_();
/// The next complete line, valid until the next call, or nullptr
const char *read_message_();
void read_state_();
/// Run the command at the front of the queue; true when it is done
bool run_command_(const Command &cmd, uint32_t now);
/// Handle a reply to a sent command; true when the command is done
bool handle_reply_(const Command &cmd);
bool handle_reply_(const Command &cmd, const char *message);
void set_detected_(bool detected);
};
@@ -1,5 +1,6 @@
#include "ds248x_one_wire_bus.h"
#include "ds248x.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::ds248x {
@@ -151,11 +152,7 @@ uint64_t DS248xOneWireBus::search_int() {
}
// Update address based on direction taken
if (dir_taken) {
address |= bit_mask;
} else {
address &= ~bit_mask;
}
assign_bits(address, bit_mask, dir_taken);
}
// Search successful
+4 -6
View File
@@ -18,13 +18,11 @@ E131AddressableLightEffect = e131_ns.class_(
)
E131Component = e131_ns.class_("E131Component", cg.Component)
METHODS = {"UNICAST": e131_ns.E131_UNICAST, "MULTICAST": e131_ns.E131_MULTICAST}
E131ListenMethod = e131_ns.enum("E131ListenMethod")
METHODS = E131ListenMethod.options("UNICAST", "MULTICAST", prefix="E131_")
CHANNELS = {
"MONO": e131_ns.E131_MONO,
"RGB": e131_ns.E131_RGB,
"RGBW": e131_ns.E131_RGBW,
}
E131LightChannels = e131_ns.enum("E131LightChannels")
CHANNELS = E131LightChannels.options("MONO", "RGB", "RGBW", prefix="E131_")
CONF_UNIVERSE = "universe"
CONF_E131_ID = "e131_id"
@@ -20,9 +20,7 @@ void EKTF2232Touchscreen::setup() {
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
this->reset_pin_->setup();
this->hard_reset_();
pulse_reset(this->reset_pin_, {.low_ms = 15, .high_after_ms = 15});
if (!this->soft_reset_()) {
ESP_LOGE(TAG, "Failed to soft reset EKT2232!");
this->interrupt_pin_->detach_interrupt();
@@ -96,13 +94,6 @@ bool EKTF2232Touchscreen::get_power_state() {
return (received[1] >> 3) & 1;
}
void EKTF2232Touchscreen::hard_reset_() {
this->reset_pin_->digital_write(false);
delay(15);
this->reset_pin_->digital_write(true);
delay(15);
}
bool EKTF2232Touchscreen::soft_reset_() {
auto err = this->write(SOFT_RESET_CMD, 4);
if (err != i2c::ERROR_OK)
@@ -22,7 +22,6 @@ class EKTF2232Touchscreen final : public Touchscreen, public i2c::I2CDevice {
bool get_power_state();
protected:
void hard_reset_();
bool soft_reset_();
void update_touches() override;
+12 -15
View File
@@ -4,21 +4,18 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::epaper_spi {
ESPHOME_LOG_TAG(TAG, "epaper_spi");
static constexpr size_t EPAPER_MAX_CMD_LOG_BYTES = 128;
static constexpr const char *const EPAPER_STATE_STRINGS[] = {
"IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE",
"TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP",
};
PROGMEM_STRING_TABLE(EPaperStateStrings, "IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE",
"TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", "Unknown");
const char *EPaperBase::epaper_state_to_string_() {
if (auto idx = static_cast<unsigned>(this->state_); idx < std::size(EPAPER_STATE_STRINGS))
return EPAPER_STATE_STRINGS[idx];
return "Unknown";
const LogString *EPaperBase::epaper_state_to_string_() {
return EPaperStateStrings::get_log_str(static_cast<uint8_t>(this->state_), EPaperStateStrings::LAST_INDEX);
}
void EPaperBase::setup() {
@@ -159,7 +156,7 @@ void EPaperBase::update_effective_transform_() {
void EPaperBase::update() {
if (this->state_ != EPaperState::IDLE) {
ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_());
ESP_LOGE(TAG, "Display already in state %s", LOG_STR_ARG(epaper_state_to_string_()));
return;
}
this->set_state_(EPaperState::UPDATE);
@@ -212,7 +209,7 @@ void EPaperBase::loop() {
this->high_frequency_loop_.stop();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (now - this->waiting_for_idle_last_print_ >= 1000) {
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
ESP_LOGV(TAG, "Waiting for idle in state %s", LOG_STR_ARG(this->epaper_state_to_string_()));
this->waiting_for_idle_last_print_ = millis();
}
#endif
@@ -227,7 +224,7 @@ void EPaperBase::loop() {
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
void EPaperBase::abandon_update_() {
ESP_LOGE(TAG, "Display stayed busy for %u s in state %s; check the busy pin", (unsigned) this->busy_timeout_seconds_,
this->epaper_state_to_string_());
LOG_STR_ARG(this->epaper_state_to_string_()));
this->status_set_warning(LOG_STR("Busy pin timeout"));
if (this->state_ == EPaperState::TRANSFER_DATA)
this->abort_transfer();
@@ -247,10 +244,10 @@ void EPaperBase::abandon_update_() {
* Should a subclassed class need to override this, the method will need to be made virtual.
*/
void EPaperBase::process_state_() {
ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_());
ESP_LOGV(TAG, "Process state entered in state %s", LOG_STR_ARG(epaper_state_to_string_()));
switch (this->state_) {
default:
ESP_LOGE(TAG, "Display is in unhandled state %s", epaper_state_to_string_());
ESP_LOGE(TAG, "Display is in unhandled state %s", LOG_STR_ARG(epaper_state_to_string_()));
this->set_state_(EPaperState::IDLE);
break;
case EPaperState::IDLE:
@@ -342,7 +339,7 @@ void EPaperBase::process_state_() {
}
void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
ESP_LOGV(TAG, "Exit state %s", LOG_STR_ARG(this->epaper_state_to_string_()));
this->state_ = state;
// allow subclasses to nominate delays
if (delay == 0)
@@ -350,7 +347,7 @@ void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
this->next_delay_ = 0;
this->delay_until_ = millis() + delay;
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", LOG_STR_ARG(this->epaper_state_to_string_()), delay,
TRUEFALSE(this->waiting_for_idle_));
// Each step of an update otherwise waits for the next loop iteration; loop() slows down again for long waits
if (state == EPaperState::IDLE) {
+1 -1
View File
@@ -125,7 +125,7 @@ class EPaperBase : public Display,
bool is_using_partial_update_() const { return this->full_update_every_ > 1; }
void process_state_();
const char *epaper_state_to_string_();
const LogString *epaper_state_to_string_();
bool is_idle_() const;
void setup_pins_() const;
virtual bool reset();
@@ -72,9 +72,7 @@ bool EPaperJD79660::reset() {
// As commented on SLEEP_MS_RESET1: Reset pulse must happen within time window.
// So do not use FSM loop, and avoid other calls/logs during pulse below.
this->reset_pin_->digital_write(false);
delay(SLEEP_MS_RESET1);
this->reset_pin_->digital_write(true);
pulse_reset(this->reset_pin_, {.low_ms = SLEEP_MS_RESET1});
this->reset_duration_ = SLEEP_MS_RESET2;
this->step_ = FSMState::RESET_STEP2_IDLECHECK;
+42 -36
View File
@@ -33,53 +33,59 @@ void ES7210::dump_config() {
this->channel_gains_[2], this->channel_gains_[3]);
}
static constexpr i2c::I2CRegValue ES7210_RESET[] PROGMEM = {
// Software reset
{ES7210_RESET_REG00, 0xff},
{ES7210_RESET_REG00, 0x32},
{ES7210_CLOCK_OFF_REG01, 0x3f},
// Set initialization time when device powers up
{ES7210_TIME_CONTROL0_REG09, 0x30},
{ES7210_TIME_CONTROL1_REG0A, 0x30},
// Configure HFP for all ADC channels
{ES7210_ADC12_HPF2_REG23, 0x2a},
{ES7210_ADC12_HPF1_REG22, 0x0a},
{ES7210_ADC34_HPF2_REG20, 0x0a},
{ES7210_ADC34_HPF1_REG21, 0x2a},
};
static constexpr i2c::I2CRegValue ES7210_ANALOG[] PROGMEM = {
// Configure analog power
{ES7210_ANALOG_REG40, 0xC3},
// Set mic bias
{ES7210_MIC12_BIAS_REG41, 0x70},
{ES7210_MIC34_BIAS_REG42, 0x70},
};
static constexpr i2c::I2CRegValue ES7210_POWER_ON[] PROGMEM = {
// Power on mics 1 through 4
{ES7210_MIC1_POWER_REG47, 0x08},
{ES7210_MIC2_POWER_REG48, 0x08},
{ES7210_MIC3_POWER_REG49, 0x08},
{ES7210_MIC4_POWER_REG4A, 0x08},
// Power down DLL
{ES7210_POWER_DOWN_REG06, 0x04},
// Power on MIC1-4 bias & ADC1-4 & PGA1-4 Power
{ES7210_MIC12_POWER_REG4B, 0x0F},
{ES7210_MIC34_POWER_REG4C, 0x0F},
// Enable device
{ES7210_RESET_REG00, 0x71},
{ES7210_RESET_REG00, 0x41},
};
void ES7210::setup() {
// 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));
ES7210_ERROR_FAILED(this->write_registers(ES7210_RESET));
// Secondary I2S mode settings
ES7210_ERROR_FAILED(this->update_register(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));
ES7210_ERROR_FAILED(this->write_registers(ES7210_ANALOG));
// 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));
ES7210_ERROR_FAILED(this->write_registers(ES7210_POWER_ON));
this->setup_complete_ = true;
}
+54 -48
View File
@@ -25,56 +25,62 @@ ESPHOME_LOG_TAG(TAG, "es7243e");
void ES7243E::dump_config() { ESP_LOGCONFIG(TAG, "ES7243E audio ADC:"); }
// clang-format off
static constexpr i2c::I2CRegValue ES7243E_INIT[] PROGMEM = {
{ES7243E_CLOCK_MGR_REG01, 0x3A},
{ES7243E_RESET_REG00, 0x80},
{ES7243E_TEST_MODE_REGF9, 0x00},
{ES7243E_CLOCK_MGR_REG04, 0x02},
{ES7243E_CLOCK_MGR_REG04, 0x01},
{ES7243E_TEST_MODE_REGF9, 0x01},
{ES7243E_RESET_REG00, 0x1E},
{ES7243E_CLOCK_MGR_REG01, 0x00},
{ES7243E_CLOCK_MGR_REG02, 0x00},
{ES7243E_CLOCK_MGR_REG03, 0x20},
{ES7243E_CLOCK_MGR_REG04, 0x01},
{ES7243E_ADC_CTRL_REG0D, 0x00},
{ES7243E_CLOCK_MGR_REG05, 0x00},
{ES7243E_CLOCK_MGR_REG06, 0x03}, // SCLK=MCLK/4
{ES7243E_CLOCK_MGR_REG07, 0x00}, // LRCK=MCLK/256
{ES7243E_CLOCK_MGR_REG08, 0xFF}, // LRCK=MCLK/256
{ES7243E_CLOCK_MGR_REG09, 0xCA},
{ES7243E_SDP_REG0A, 0x85},
{ES7243E_SDP_REG0B, 0x00},
{ES7243E_ADC_CTRL_REG0E, 0xBF},
{ES7243E_ADC_CTRL_REG0F, 0x80},
{ES7243E_ADC_CTRL_REG14, 0x0C},
{ES7243E_ADC_CTRL_REG15, 0x0C},
{ES7243E_ANALOG_REG17, 0x02},
{ES7243E_ANALOG_REG18, 0x26},
{ES7243E_ANALOG_REG19, 0x77},
{ES7243E_ANALOG_REG1A, 0xF4},
{ES7243E_ANALOG_REG1B, 0x66},
{ES7243E_ANALOG_REG1C, 0x44},
{ES7243E_ANALOG_REG1E, 0x00},
{ES7243E_ANALOG_REG1F, 0x0C},
{ES7243E_ANALOG_REG20, 0x1A}, // PGA gain +30dB
{ES7243E_ANALOG_REG21, 0x1A},
{ES7243E_RESET_REG00, 0x80},
{ES7243E_CLOCK_MGR_REG01, 0x3A},
{ES7243E_ANALOG_REG16, 0x3F},
{ES7243E_ANALOG_REG16, 0x00},
{ES7243E_TEST_MODE_REGF9, 0x00},
{ES7243E_CLOCK_MGR_REG04, 0x01},
{ES7243E_ANALOG_REG17, 0x01},
};
// clang-format on
static constexpr i2c::I2CRegValue ES7243E_START[] PROGMEM = {
{ES7243E_RESET_REG00, 0x80},
{ES7243E_CLOCK_MGR_REG01, 0x3A},
{ES7243E_ANALOG_REG16, 0x3F},
{ES7243E_ANALOG_REG16, 0x00},
};
void ES7243E::setup() {
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x3A));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x80));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_TEST_MODE_REGF9, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG04, 0x02));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG04, 0x01));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_TEST_MODE_REGF9, 0x01));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x1E));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG02, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG03, 0x20));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG04, 0x01));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG0D, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG05, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG06, 0x03)); // SCLK=MCLK/4
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG07, 0x00)); // LRCK=MCLK/256
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG08, 0xFF)); // LRCK=MCLK/256
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG09, 0xCA));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_SDP_REG0A, 0x85));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_SDP_REG0B, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG0E, 0xBF));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG0F, 0x80));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG14, 0x0C));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG15, 0x0C));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG17, 0x02));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG18, 0x26));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG19, 0x77));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1A, 0xF4));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1B, 0x66));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1C, 0x44));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1E, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1F, 0x0C));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG20, 0x1A)); // PGA gain +30dB
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG21, 0x1A));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x80));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x3A));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG16, 0x3F));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG16, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_TEST_MODE_REGF9, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG04, 0x01));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG17, 0x01));
ES7243E_ERROR_FAILED(this->write_registers(ES7243E_INIT));
ES7243E_ERROR_FAILED(this->configure_mic_gain_());
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x80));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x3A));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG16, 0x3F));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG16, 0x00));
ES7243E_ERROR_FAILED(this->write_registers(ES7243E_START));
this->setup_complete_ = true;
}
+46 -62
View File
@@ -9,70 +9,54 @@ namespace esphome::es8156 {
ESPHOME_LOG_TAG(TAG, "es8156");
// Mark the component as failed; use only in setup
#define ES8156_ERROR_FAILED(func) \
if (!(func)) { \
this->mark_failed(LOG_STR("Failed to initialize")); \
return; \
}
static constexpr i2c::I2CRegValue ES8156_INIT[] PROGMEM = {
// REG02 MODE CONFIG 1: Enable software mode for I2C control of volume/mute
// Bit 2: SOFT_MODE_SEL=1 (software mode enabled)
{ES8156_REG02_SCLK_MODE, 0x04},
// Analog system configuration (active-low power down bits, active-high enables)
// REG20 ANALOG SYSTEM: Configure analog signal path
{ES8156_REG20_ANALOG_SYS1, 0x2A},
// REG21 ANALOG SYSTEM: VSEL=0x1C (bias level ~120%), normal VREF ramp speed
{ES8156_REG21_ANALOG_SYS2, 0x3C},
// REG22 ANALOG SYSTEM: Line out mode (HPSW=0), OUT_MUTE=0 (not muted)
{ES8156_REG22_ANALOG_SYS3, 0x00},
// REG24 ANALOG SYSTEM: Low power mode for VREFBUF, HPCOM, DACVRP; DAC normal power
// Bits 2:0 = 0x07: LPVREFBUF=1, LPHPCOM=1, LPDACVRP=1, LPDAC=0
{ES8156_REG24_ANALOG_LP, 0x07},
// REG23 ANALOG SYSTEM: Lowest bias (IBIAS_SW=0), VMIDLVL=VDDA/2, normal impedance
{ES8156_REG23_ANALOG_SYS4, 0x00},
// Timing and interface configuration
// REG0A/0B TIME CONTROL: Fast state machine transitions
{ES8156_REG0A_TIME_CONTROL1, 0x01},
{ES8156_REG0B_TIME_CONTROL2, 0x01},
// REG11 SDP INTERFACE CONFIG: Default I2S format (24-bit, I2S mode)
{ES8156_REG11_DAC_SDP, 0x00},
// REG19 EQ CONTROL 1: EQ disabled (EQ_ON=0), EQ_BAND_NUM=2
{ES8156_REG19_EQ_CONTROL1, 0x20},
// REG0D P2S CONTROL: Parallel-to-serial converter settings
{ES8156_REG0D_P2S_CONTROL, 0x14},
// REG09 MISC CONTROL 2: Default settings
{ES8156_REG09_MISC_CONTROL2, 0x00},
// REG18 MISC CONTROL 3: Stereo channel routing, no inversion
// Bits 5:4 CHN_CROSS: 0=L→L/R→R, 1=L to both, 2=R to both, 3=swap L/R
// Bits 3:2: LCH_INV/RCH_INV channel inversion
{ES8156_REG18_MISC_CONTROL3, 0x00},
// REG08 CLOCK OFF: Enable all internal clocks (0x3F = all clock gates open)
{ES8156_REG08_CLOCK_ON_OFF, 0x3F},
// REG00 RESET CONTROL: Reset sequence
// First: RST_DIG=1 (assert digital reset)
{ES8156_REG00_RESET, 0x02},
// Then: CSM_ON=1 (enable chip state machine), RST_DIG=1
{ES8156_REG00_RESET, 0x03},
// REG25 ANALOG SYSTEM: Power up analog blocks
// VMIDSEL=2 (normal VMID operation), PDN_ANA=0, ENREFR=0, ENHPCOM=0
// PDN_DACVREFGEN=0, PDN_VREFBUF=0, PDN_DAC=0 (all enabled)
{ES8156_REG25_ANALOG_SYS5, 0x20},
};
void ES8156::setup() {
// REG02 MODE CONFIG 1: Enable software mode for I2C control of volume/mute
// Bit 2: SOFT_MODE_SEL=1 (software mode enabled)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG02_SCLK_MODE, 0x04));
// Analog system configuration (active-low power down bits, active-high enables)
// REG20 ANALOG SYSTEM: Configure analog signal path
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG20_ANALOG_SYS1, 0x2A));
// REG21 ANALOG SYSTEM: VSEL=0x1C (bias level ~120%), normal VREF ramp speed
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG21_ANALOG_SYS2, 0x3C));
// REG22 ANALOG SYSTEM: Line out mode (HPSW=0), OUT_MUTE=0 (not muted)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG22_ANALOG_SYS3, 0x00));
// REG24 ANALOG SYSTEM: Low power mode for VREFBUF, HPCOM, DACVRP; DAC normal power
// Bits 2:0 = 0x07: LPVREFBUF=1, LPHPCOM=1, LPDACVRP=1, LPDAC=0
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG24_ANALOG_LP, 0x07));
// REG23 ANALOG SYSTEM: Lowest bias (IBIAS_SW=0), VMIDLVL=VDDA/2, normal impedance
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG23_ANALOG_SYS4, 0x00));
// Timing and interface configuration
// REG0A/0B TIME CONTROL: Fast state machine transitions
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0A_TIME_CONTROL1, 0x01));
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0B_TIME_CONTROL2, 0x01));
// REG11 SDP INTERFACE CONFIG: Default I2S format (24-bit, I2S mode)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG11_DAC_SDP, 0x00));
// REG19 EQ CONTROL 1: EQ disabled (EQ_ON=0), EQ_BAND_NUM=2
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG19_EQ_CONTROL1, 0x20));
// REG0D P2S CONTROL: Parallel-to-serial converter settings
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0D_P2S_CONTROL, 0x14));
// REG09 MISC CONTROL 2: Default settings
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG09_MISC_CONTROL2, 0x00));
// REG18 MISC CONTROL 3: Stereo channel routing, no inversion
// Bits 5:4 CHN_CROSS: 0=L→L/R→R, 1=L to both, 2=R to both, 3=swap L/R
// Bits 3:2: LCH_INV/RCH_INV channel inversion
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG18_MISC_CONTROL3, 0x00));
// REG08 CLOCK OFF: Enable all internal clocks (0x3F = all clock gates open)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG08_CLOCK_ON_OFF, 0x3F));
// REG00 RESET CONTROL: Reset sequence
// First: RST_DIG=1 (assert digital reset)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x02));
// Then: CSM_ON=1 (enable chip state machine), RST_DIG=1
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x03));
// REG25 ANALOG SYSTEM: Power up analog blocks
// VMIDSEL=2 (normal VMID operation), PDN_ANA=0, ENREFR=0, ENHPCOM=0
// PDN_DACVREFGEN=0, PDN_VREFBUF=0, PDN_DAC=0 (all enabled)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG25_ANALOG_SYS5, 0x20));
if (!this->write_registers(ES8156_INIT))
this->mark_failed(LOG_STR("Failed to initialize"));
}
void ES8156::dump_config() { ESP_LOGCONFIG(TAG, "ES8156 Audio Codec:"); }
+69 -78
View File
@@ -22,79 +22,76 @@ ESPHOME_LOG_TAG(TAG, "es8388");
return false; \
}
static constexpr i2c::I2CRegValue ES8388_INIT[] PROGMEM = {
// I2S worker mode
{ES8388_MASTERMODE, 0x00},
/* Chip Control and Power Management */
{ES8388_CONTROL2, 0x50},
// normal all and power up all
{ES8388_CHIPPOWER, 0x00},
// vmidsel/500k
// EnRef=0,Play&Record Mode,(0x17-both of mic&play)
{ES8388_CONTROL1, 0x12},
// i2s 16 bits
{ES8388_DACCONTROL1, 0x18},
// sample freq 256
// DACFsMode,SINGLE SPEED; DACFsRatio,256
{ES8388_DACCONTROL2, 0x02},
// 0x00 audio on LIN1&RIN1, 0x09 LIN2&RIN2
{ES8388_DACCONTROL16, 0x00},
// only left DAC to left mixer enable 0db
{ES8388_DACCONTROL17, 0x90},
// only right DAC to right mixer enable 0db
{ES8388_DACCONTROL20, 0x90},
// set internal ADC and DAC use the same LRCK clock, ADC LRCK as internal LRCK
{ES8388_DACCONTROL21, 0x80},
// vroi=0 - 1.5k VREF to analog output resistance (default)
{ES8388_DACCONTROL23, 0x00},
// power down adc and line in
{ES8388_ADCPOWER, 0xFF},
//@nightdav
{ES8388_ADCCONTROL1, 0x00}, // +21dB : recommended value for ALC & voice recording
// set to Mono Right
{ES8388_ADCCONTROL3, 0x02},
// I2S 16 Bits length and I2S serial audio data format
{ES8388_ADCCONTROL4, 0x0d},
// ADCFsMode,singel SPEED,RATIO=256
{ES8388_ADCCONTROL5, 0x02},
// ADC Volume
{ES8388_ADCCONTROL8, 0x00},
{ES8388_ADCCONTROL9, 0x00},
//@nightDav
// ALC Config (as recommended by ES8388 user guide for voice recording)
// Reg 0x12 = 0xe2 (ALC enable, PGA Max. Gain=23.5dB, Min. Gain=0dB)
{ES8388_ADCCONTROL10, 0xe2},
// Reg 0x13 = 0xa0 (ALC Target=-1.5dB, ALC Hold time =0 mS)
{ES8388_ADCCONTROL11, 0xa0},
// Reg 0x14 = 0x12(Decay time =820uS , Attack time = 416 uS)
{ES8388_ADCCONTROL12, 0x12},
// Reg 0x15 = 0x06(ALC mode)
{ES8388_ADCCONTROL13, 0x06},
// Reg 0x16 = 0xc3(nose gate = -40.5dB, NGG = 0x01(mute ADC))
{ES8388_ADCCONTROL14, 0xc3},
// Power on ADC
{ES8388_DACCONTROL21, 0x80},
// Start state machine
{ES8388_CHIPPOWER, 0xF0},
};
static constexpr i2c::I2CRegValue ES8388_UNMUTE[] PROGMEM = {
// unmute ADC with fade in
{ES8388_ADCCONTROL7, 0x60},
// unmute DAC with fade in
{ES8388_DACCONTROL3, 0x20},
// Power on ADC, Enable LIN&RIN, Power off MICBIAS, set int1lp to low power mode
{ES8388_ADCPOWER, 0x09},
};
void ES8388::setup() {
// mute DAC
this->set_mute_state_(true);
// I2S worker mode
ES8388_ERROR_FAILED(this->write_byte(ES8388_MASTERMODE, 0x00));
/* Chip Control and Power Management */
ES8388_ERROR_FAILED(this->write_byte(ES8388_CONTROL2, 0x50));
// normal all and power up all
ES8388_ERROR_FAILED(this->write_byte(ES8388_CHIPPOWER, 0x00));
// vmidsel/500k
// EnRef=0,Play&Record Mode,(0x17-both of mic&play)
ES8388_ERROR_FAILED(this->write_byte(ES8388_CONTROL1, 0x12));
// i2s 16 bits
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL1, 0x18));
// sample freq 256
// DACFsMode,SINGLE SPEED; DACFsRatio,256
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL2, 0x02));
// 0x00 audio on LIN1&RIN1, 0x09 LIN2&RIN2
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL16, 0x00));
// only left DAC to left mixer enable 0db
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL17, 0x90));
// only right DAC to right mixer enable 0db
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL20, 0x90));
// set internal ADC and DAC use the same LRCK clock, ADC LRCK as internal LRCK
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL21, 0x80));
// vroi=0 - 1.5k VREF to analog output resistance (default)
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL23, 0x00));
// power down adc and line in
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCPOWER, 0xFF));
//@nightdav
ES8388_ERROR_FAILED(
this->write_byte(ES8388_ADCCONTROL1, 0x00)); // +21dB : recommended value for ALC & voice recording
// set to Mono Right
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL3, 0x02));
// I2S 16 Bits length and I2S serial audio data format
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL4, 0x0d));
// ADCFsMode,singel SPEED,RATIO=256
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL5, 0x02));
// ADC Volume
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL8, 0x00));
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL9, 0x00));
//@nightDav
// ALC Config (as recommended by ES8388 user guide for voice recording)
// Reg 0x12 = 0xe2 (ALC enable, PGA Max. Gain=23.5dB, Min. Gain=0dB)
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL10, 0xe2));
// Reg 0x13 = 0xa0 (ALC Target=-1.5dB, ALC Hold time =0 mS)
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL11, 0xa0));
// Reg 0x14 = 0x12(Decay time =820uS , Attack time = 416 uS)
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL12, 0x12));
// Reg 0x15 = 0x06(ALC mode)
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL13, 0x06));
// Reg 0x16 = 0xc3(nose gate = -40.5dB, NGG = 0x01(mute ADC))
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL14, 0xc3));
// Power on ADC
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL21, 0x80));
// Start state machine
ES8388_ERROR_FAILED(this->write_byte(ES8388_CHIPPOWER, 0xF0));
ES8388_ERROR_FAILED(this->write_registers(ES8388_INIT));
delay(1);
ES8388_ERROR_FAILED(this->write_byte(ES8388_CHIPPOWER, 0x00));
@@ -104,13 +101,7 @@ void ES8388::setup() {
this->set_mute_state_(false);
// unmute ADC with fade in
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL7, 0x60));
// unmute DAC with fade in
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL3, 0x20));
// Power on ADC, Enable LIN&RIN, Power off MICBIAS, set int1lp to low power mode
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCPOWER, 0x09));
ES8388_ERROR_FAILED(this->write_registers(ES8388_UNMUTE));
#ifdef USE_ES8388_SELECT
if (this->dac_output_select_ != nullptr) {
@@ -152,8 +143,8 @@ void ES8388::dump_config() {
ESP_LOGCONFIG(TAG, "ES8388 Audio Codec:");
LOG_I2C_DEVICE(this);
#ifdef USE_ES8388_SELECT
LOG_SELECT(" ", "DacOutputSelect", this->dac_output_select_);
LOG_SELECT(" ", "ADCInputMicSelect", this->adc_input_mic_select_);
LOG_SELECT(" ", "DAC Output", this->dac_output_select_);
LOG_SELECT(" ", "ADC Input Mic", this->adc_input_mic_select_);
#endif
}
+9 -5
View File
@@ -24,14 +24,12 @@ ADC_INPUT_MIC_OPTIONS = AdcInputMicLine.options(
)
ADC_INPUT_MIC_OPTION_NAMES = list(ADC_INPUT_MIC_OPTIONS)
DacOutputSelect = es8388_ns.class_("DacOutputSelect", select.Select)
ADCInputMicSelect = es8388_ns.class_("ADCInputMicSelect", select.Select)
CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(CONF_ES8388_ID): cv.use_id(ES8388),
cv.Optional(CONF_DAC_OUTPUT): select.select_schema(
DacOutputSelect,
select.ApplySelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_CHIP,
).extend(
@@ -42,7 +40,7 @@ CONFIG_SCHEMA = cv.All(
}
),
cv.Optional(CONF_ADC_INPUT_MIC): select.select_schema(
ADCInputMicSelect,
select.ApplySelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_CHIP,
).extend(
@@ -61,12 +59,18 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_ES8388_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_DAC_OUTPUT, hub.set_dac_output_select, options=DAC_OUTPUT_OPTION_NAMES
CONF_DAC_OUTPUT,
hub.set_dac_output_select,
options=DAC_OUTPUT_OPTION_NAMES,
apply="set_dac_output(static_cast<es8388::DacOutputLine>(index))",
publish="before",
)
await selects(
CONF_ADC_INPUT_MIC,
hub.set_adc_input_mic_select,
options=ADC_INPUT_MIC_OPTION_NAMES,
apply="set_adc_input_mic(static_cast<es8388::AdcInputMicLine>(index))",
publish="before",
)
if (
initial := config.get(CONF_DAC_OUTPUT, {}).get(CONF_INITIAL_OPTION)
@@ -1,10 +0,0 @@
#include "adc_input_mic_select.h"
namespace esphome::es8388 {
void ADCInputMicSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_adc_input_mic(static_cast<AdcInputMicLine>(index));
}
} // namespace esphome::es8388
@@ -1,13 +0,0 @@
#pragma once
#include "esphome/components/es8388/es8388.h"
#include "esphome/components/select/select.h"
namespace esphome::es8388 {
class ADCInputMicSelect final : public select::Select, public Parented<ES8388> {
protected:
void control(size_t index) override;
};
} // namespace esphome::es8388
@@ -1,10 +0,0 @@
#include "dac_output_select.h"
namespace esphome::es8388 {
void DacOutputSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_dac_output(static_cast<DacOutputLine>(index));
}
} // namespace esphome::es8388
@@ -1,13 +0,0 @@
#pragma once
#include "esphome/components/es8388/es8388.h"
#include "esphome/components/select/select.h"
namespace esphome::es8388 {
class DacOutputSelect final : public select::Select, public Parented<ES8388> {
protected:
void control(size_t index) override;
};
} // namespace esphome::es8388
@@ -114,7 +114,8 @@ BLECharacteristicNotifyAction = esp32_ble_server_automations_ns.class_(
"BLECharacteristicNotifyAction", automation.Action
)
bytebuffer_ns = cg.esphome_ns.namespace("bytebuffer")
Endianness_ns = bytebuffer_ns.namespace("Endian")
# Unscoped in C++; is_class keeps the generated Endian::LITTLE spelling
Endian = bytebuffer_ns.enum("Endian", is_class=True)
PROPERTY_MAP = {
@@ -410,10 +411,7 @@ def value_schema(default_type=UNDEFINED, templatable=True):
),
cv.Optional(CONF_STRING_ENCODING, default="utf_8"): validate_encoding,
cv.Optional(CONF_ENDIANNESS, default="LITTLE"): cv.enum(
{
"LITTLE": Endianness_ns.LITTLE,
"BIG": Endianness_ns.BIG,
}
Endian.options("LITTLE", "BIG")
),
},
validate_value_type,
+13 -17
View File
@@ -54,25 +54,21 @@ CAN_MODES = {
# The supported bit rates differ between ESP32 variants.
# See ESP-IDF Programming Guide --> API Reference --> Two-Wire Automotive Interface (TWAI)
CAN_SPEEDS_ESP32 = {
"20KBPS": CanSpeed.CAN_20KBPS,
"25KBPS": CanSpeed.CAN_25KBPS,
"50KBPS": CanSpeed.CAN_50KBPS,
"100KBPS": CanSpeed.CAN_100KBPS,
"125KBPS": CanSpeed.CAN_125KBPS,
"250KBPS": CanSpeed.CAN_250KBPS,
"500KBPS": CanSpeed.CAN_500KBPS,
"800KBPS": CanSpeed.CAN_800KBPS,
"1000KBPS": CanSpeed.CAN_1000KBPS,
}
CAN_SPEEDS_ESP32 = CanSpeed.options(
"20KBPS",
"25KBPS",
"50KBPS",
"100KBPS",
"125KBPS",
"250KBPS",
"500KBPS",
"800KBPS",
"1000KBPS",
prefix="CAN_",
)
CAN_SPEEDS_ESP32_S2 = {
"1KBPS": CanSpeed.CAN_1KBPS,
"5KBPS": CanSpeed.CAN_5KBPS,
"10KBPS": CanSpeed.CAN_10KBPS,
"12K5BPS": CanSpeed.CAN_12K5BPS,
"16KBPS": CanSpeed.CAN_16KBPS,
"20KBPS": CanSpeed.CAN_20KBPS,
**CanSpeed.options("1KBPS", "5KBPS", "10KBPS", "12K5BPS", "16KBPS", prefix="CAN_"),
**CAN_SPEEDS_ESP32,
}
@@ -1,4 +1,5 @@
#include "led_strip.h"
#include <algorithm>
#include <cinttypes>
#ifdef USE_ESP32
@@ -14,6 +15,8 @@ namespace esphome::esp32_rmt_led_strip {
ESPHOME_LOG_TAG(TAG, "esp32_rmt_led_strip");
static const size_t RMT_SYMBOLS_PER_BYTE = 8;
// Idle time the line must stay low between frames so the strip latches
static constexpr uint32_t RESET_GAP_US = 50;
// Query the RMT default clock source frequency. This varies by variant:
// APB (80MHz) on ESP32/S2/S3/C3, PLL_F80M (80MHz) on C6/P4, XTAL (32MHz) on H2.
@@ -91,10 +94,20 @@ void ESP32RMTLEDStripLightOutput::setup() {
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1);
#endif
const uint32_t resolution_hz = rmt_resolution_hz();
// Longest wire time of one frame; write_state() defers instead of blocking until it has elapsed
uint32_t bit_ticks = std::max(this->params_.bit0.duration0 + this->params_.bit0.duration1,
this->params_.bit1.duration0 + this->params_.bit1.duration1);
uint64_t frame_ticks = (uint64_t) bit_ticks * buffer_size * RMT_SYMBOLS_PER_BYTE + this->params_.reset.duration0 +
this->params_.reset.duration1;
this->frame_time_us_ = frame_ticks * 1000000 / resolution_hz + RESET_GAP_US;
// Seed with the interval write_state() gates on, so the first frame is not deferred
this->last_refresh_ = micros() - std::max(this->frame_time_us_, this->max_refresh_rate_);
rmt_tx_channel_config_t channel;
memset(&channel, 0, sizeof(channel));
channel.clk_src = RMT_CLK_SRC_DEFAULT;
channel.resolution_hz = rmt_resolution_hz();
channel.resolution_hz = resolution_hz;
channel.gpio_num = gpio_num_t(this->pin_);
channel.mem_block_symbols = this->rmt_symbols_;
channel.trans_queue_depth = 1;
@@ -157,22 +170,21 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi
}
void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
uint32_t now = micros();
if (this->throttle_show_(now, this->last_refresh_, this->max_refresh_rate_))
// Previous frame still on the wire, inside its reset gap, or refreshing too often: try again next loop
if (this->throttle_show_(micros(), this->last_refresh_, std::max(this->frame_time_us_, this->max_refresh_rate_)))
return;
this->last_refresh_ = now;
// The gate above should always cover the previous frame; never overwrite rmt_buf_ while the RMT still reads it
if (rmt_tx_wait_all_done(this->channel_, 0) != ESP_OK) {
ESP_LOGE(TAG, "RMT TX still busy");
this->status_set_warning();
this->schedule_show();
return;
}
this->mark_shown_();
ESP_LOGVV(TAG, "Writing RGB values to bus");
esp_err_t error = rmt_tx_wait_all_done(this->channel_, 1000);
if (error != ESP_OK) {
ESP_LOGE(TAG, "RMT TX timeout");
this->status_set_warning();
return;
}
delayMicroseconds(50);
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
memcpy(this->rmt_buf_, this->buf_, this->get_buffer_size_());
#else
@@ -203,13 +215,19 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, this->get_buffer_size_(), &config);
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, this->get_buffer_size_(), &config);
#else
error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config);
esp_err_t error =
rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config);
#endif
if (!this->status_comm_result(error == ESP_OK, nullptr)) {
ESP_LOGE(TAG, "RMT TX error");
// retry next loop iteration so the frame is not lost
this->schedule_show();
return;
}
// Stamped after the transfer started so the gate above stays conservative
this->last_refresh_ = micros();
}
void ESP32RMTLEDStripLightOutput::dump_config() {
@@ -81,6 +81,8 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
uint32_t last_refresh_{0};
uint32_t max_refresh_rate_{0};
// Worst-case wire time of one frame plus the reset gap, computed in setup()
uint32_t frame_time_us_{0};
};
} // namespace esphome::esp32_rmt_led_strip
@@ -9,24 +9,23 @@ from .. import (
CONF_ESPECTRE_ID,
DOMAIN,
ESPectreComponent,
espectre_ns,
supported_traffic_generator_modes,
)
DEPENDENCIES = ["espectre"]
TrafficModeSelect = espectre_ns.class_(
"TrafficModeSelect", select.Select, cg.Parented.template(ESPectreComponent)
)
CONFIG_SCHEMA = select.select_schema(
TrafficModeSelect, entity_category=ENTITY_CATEGORY_CONFIG
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
).extend({cv.GenerateID(CONF_ESPECTRE_ID): cv.use_id(ESPectreComponent)})
async def to_code(config: ConfigType) -> None:
options = supported_traffic_generator_modes(CORE.config[DOMAIN])
var = await select.new_select(config, options=options)
await cg.register_parented(var, config[CONF_ESPECTRE_ID])
parent = await cg.get_variable(config[CONF_ESPECTRE_ID])
cg.add(parent.set_traffic_mode_select(var))
hub = await cg.get_variable(config[CONF_ESPECTRE_ID])
await select.sub_selects(config, parent=hub).create(
config,
hub.set_traffic_mode_select,
options=options,
apply="request_traffic_generator_mode(option)",
)
@@ -1,11 +0,0 @@
#include "espectre_select.h"
#ifdef USE_ESPECTRE
namespace esphome::espectre {
void TrafficModeSelect::control(size_t index) { this->parent_->request_traffic_generator_mode(this->option_at(index)); }
} // namespace esphome::espectre
#endif // USE_ESPECTRE
@@ -1,19 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESPECTRE
#include "esphome/components/select/select.h"
#include "../espectre.h"
namespace esphome::espectre {
class TrafficModeSelect final : public select::Select, public Parented<ESPectreComponent> {
protected:
void control(size_t index) override;
};
} // namespace esphome::espectre
#endif // USE_ESPECTRE
+12 -9
View File
@@ -1,14 +1,14 @@
#include "ezo.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/progmem.h"
namespace esphome::ezo {
static const char *const EZO_COMMAND_TYPE_STRINGS[] = {"EZO_READ", "EZO_LED", "EZO_DEVICE_INFORMATION",
"EZO_SLOPE", "EZO_CALIBRATION", "EZO_SLEEP",
"EZO_I2C", "EZO_T", "EZO_CUSTOM"};
PROGMEM_STRING_TABLE(EzoCommandTypeStrings, "EZO_READ", "EZO_LED", "EZO_DEVICE_INFORMATION", "EZO_SLOPE",
"EZO_CALIBRATION", "EZO_SLEEP", "EZO_I2C", "EZO_T", "EZO_CUSTOM");
static const char *const EZO_CALIBRATION_TYPE_STRINGS[] = {"LOW", "MID", "HIGH"};
PROGMEM_STRING_TABLE(EzoCalibrationTypeStrings, "LOW", "MID", "HIGH");
void EZOSensor::dump_config() {
LOG_SENSOR("", "EZO", this);
@@ -103,7 +103,8 @@ void EZOSensor::loop() {
break;
}
ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", &buf[1], EZO_COMMAND_TYPE_STRINGS[to_run->command_type]);
ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", &buf[1],
LOG_STR_ARG(EzoCommandTypeStrings::get_log_str(to_run->command_type, EzoCommandTypeStrings::LAST_INDEX)));
if (buf[0] == 1) {
std::string payload = reinterpret_cast<char *>(&buf[1]);
@@ -168,7 +169,9 @@ void EZOSensor::add_command_(const char *command, EzoCommandType command_type, u
void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) {
// max 21: "Cal,"(4) + type(4) + ","(1) + float(11) + null; use 24 for safety
char payload[24];
snprintf(payload, sizeof(payload), "Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value);
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("Cal,%s,%0.2f"),
LOG_STR_ARG(EzoCalibrationTypeStrings::get_log_str(type, EzoCalibrationTypeStrings::LAST_INDEX)),
value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
}
@@ -176,7 +179,7 @@ void EZOSensor::set_address(uint8_t address) {
if (address > 0 && address < 128) {
// max 8: "I2C,"(4) + uint8(3) + null
char payload[8];
snprintf(payload, sizeof(payload), "I2C,%u", address);
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("I2C,%u"), address);
this->new_address_ = address;
this->add_command_(payload, EzoCommandType::EZO_I2C);
} else {
@@ -197,7 +200,7 @@ void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); }
void EZOSensor::set_t(float value) {
// max 14 bytes: "T,"(2) + float with "%0.2f" (up to 11 chars) + null(1); use 16 for alignment
char payload[16];
snprintf(payload, sizeof(payload), "T,%0.2f", value);
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("T,%0.2f"), value);
this->add_command_(payload, EzoCommandType::EZO_T);
}
@@ -220,7 +223,7 @@ void EZOSensor::set_calibration_point_high(float value) {
void EZOSensor::set_calibration_generic(float value) {
// exact 16 bytes: "Cal," (4) + float with "%0.2f" (up to 11 chars, e.g. "-9999999.99") + null (1) = 16
char payload[16];
snprintf(payload, sizeof(payload), "Cal,%0.2f", value);
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("Cal,%0.2f"), value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
}
+1 -12
View File
@@ -34,10 +34,7 @@ void FT63X6Touchscreen::setup() {
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_ANY_EDGE);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->hard_reset_();
}
pulse_reset(this->reset_pin_, {.low_ms = 10});
// Get touch resolution
if (this->x_raw_max_ == this->x_raw_min_) {
@@ -57,14 +54,6 @@ void FT63X6Touchscreen::setup() {
this->write_byte(FT6X36_ADDR_TOUCHRATE_ACTIVE, 0x0E);
}
void FT63X6Touchscreen::hard_reset_() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false);
delay(10);
this->reset_pin_->digital_write(true);
}
}
void FT63X6Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "FT63X6 Touchscreen:");
LOG_I2C_DEVICE(this);
-1
View File
@@ -27,7 +27,6 @@ class FT63X6Touchscreen final : public Touchscreen, public i2c::I2CDevice {
void set_threshold(uint8_t threshold) { this->threshold_ = threshold; }
protected:
void hard_reset_();
void update_touches() override;
InternalGPIOPin *interrupt_pin_{nullptr};
@@ -83,13 +83,7 @@ void GSL3670Touchscreen::update_touches() {
void GSL3670Touchscreen::clear_reg_() {
ESP_LOGD(TAG, "clear_reg");
// GPIO reset pulse
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false);
delay(1);
this->reset_pin_->digital_write(true);
delay(5);
}
pulse_reset(this->reset_pin_, {.low_ms = 1, .high_after_ms = 5});
this->write_reg8_(0x88, 0x01);
// delay(5);
@@ -106,12 +100,7 @@ void GSL3670Touchscreen::clear_reg_() {
void GSL3670Touchscreen::reset_() {
ESP_LOGD(TAG, "reset");
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false);
delay(1);
this->reset_pin_->digital_write(true);
delay(5);
}
pulse_reset(this->reset_pin_, {.low_ms = 1, .high_after_ms = 5});
this->write_reg8_(0xe4, 0x04);
+9 -17
View File
@@ -3,7 +3,11 @@ import logging
from esphome import automation
import esphome.codegen as cg
from esphome.components import climate, logger, uart
from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode
from esphome.components.climate import (
CLIMATE_MODES,
CLIMATE_PRESETS,
CLIMATE_SWING_MODES,
)
from esphome.components.logger import DOMAIN as LOGGER_DOMAIN
from esphome.components.wifi import DOMAIN as WIFI_DOMAIN
import esphome.config_validation as cv
@@ -77,31 +81,19 @@ AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = AirflowHorizontalDirection.options(
)
SUPPORTED_SWING_MODES_OPTIONS = {
"OFF": ClimateSwingMode.CLIMATE_SWING_OFF,
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
k: CLIMATE_SWING_MODES[k] for k in ("OFF", "VERTICAL", "HORIZONTAL", "BOTH")
}
SUPPORTED_CLIMATE_MODES_OPTIONS = {
"OFF": ClimateMode.CLIMATE_MODE_OFF, # always available
"HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, # always available
"COOL": ClimateMode.CLIMATE_MODE_COOL,
"HEAT": ClimateMode.CLIMATE_MODE_HEAT,
"DRY": ClimateMode.CLIMATE_MODE_DRY,
"FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
k: CLIMATE_MODES[k] for k in ("OFF", "HEAT_COOL", "COOL", "HEAT", "DRY", "FAN_ONLY")
}
SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = {
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT,
k: CLIMATE_PRESETS[k] for k in ("AWAY", "BOOST", "COMFORT")
}
SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
k: CLIMATE_PRESETS[k] for k in ("AWAY", "BOOST", "SLEEP")
}
HonControlMethod = haier_ns.enum("HonControlMethod", True)
+14 -24
View File
@@ -6,6 +6,7 @@
#include "esphome/components/wifi/wifi_component.h"
#endif
#include "haier_base.h"
#include "esphome/core/progmem.h"
using namespace esphome::climate;
using namespace esphome::uart;
@@ -19,28 +20,16 @@ constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000;
constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000;
constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400;
const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
static const char *phase_names[] = {
"SENDING_INIT_1",
"SENDING_INIT_2",
"SENDING_FIRST_STATUS_REQUEST",
"SENDING_FIRST_ALARM_STATUS_REQUEST",
"IDLE",
"SENDING_STATUS_REQUEST",
"SENDING_UPDATE_SIGNAL_REQUEST",
"SENDING_SIGNAL_LEVEL",
"SENDING_CONTROL",
"SENDING_ACTION_COMMAND",
"SENDING_ALARM_STATUS_REQUEST",
"UNKNOWN" // Should be the last!
};
static_assert(
(sizeof(phase_names) / sizeof(char *)) == (((int) ProtocolPhases::NUM_PROTOCOL_PHASES) + 1),
"Wrong phase_names array size. Please, make sure that this array is aligned with the enum ProtocolPhases");
int phase_index = (int) phase;
if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0))
phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES;
return phase_names[phase_index];
PROGMEM_STRING_TABLE(ProtocolPhaseStrings, "SENDING_INIT_1", "SENDING_INIT_2", "SENDING_FIRST_STATUS_REQUEST",
"SENDING_FIRST_ALARM_STATUS_REQUEST", "IDLE", "SENDING_STATUS_REQUEST",
"SENDING_UPDATE_SIGNAL_REQUEST", "SENDING_SIGNAL_LEVEL", "SENDING_CONTROL",
"SENDING_ACTION_COMMAND", "SENDING_ALARM_STATUS_REQUEST", "UNKNOWN");
const LogString *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
static_assert(ProtocolPhaseStrings::COUNT == ((int) ProtocolPhases::NUM_PROTOCOL_PHASES) + 1,
"ProtocolPhaseStrings must match the enum ProtocolPhases, with UNKNOWN last");
// UNKNOWN (-1) wraps past the table and reads as the last entry, "UNKNOWN"
return ProtocolPhaseStrings::get_log_str(static_cast<uint8_t>(phase), ProtocolPhaseStrings::LAST_INDEX);
}
bool check_timeout(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint,
@@ -71,7 +60,8 @@ HaierClimateBase::~HaierClimateBase() {}
void HaierClimateBase::set_phase(ProtocolPhases phase) {
if (this->protocol_phase_ != phase) {
ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase));
ESP_LOGV(TAG, "Phase transition: %s => %s", LOG_STR_ARG(phase_to_string_(this->protocol_phase_)),
LOG_STR_ARG(phase_to_string_(phase)));
this->protocol_phase_ = phase;
}
}
@@ -225,7 +215,7 @@ haier_protocol::HandlerError HaierClimateBase::report_network_status_answer_hand
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(haier_protocol::FrameType request_type) {
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) request_type,
phase_to_string_(this->protocol_phase_));
LOG_STR_ARG(phase_to_string_(this->protocol_phase_)));
if (this->protocol_phase_ > ProtocolPhases::IDLE) {
this->set_phase(ProtocolPhases::IDLE);
} else {
+1 -1
View File
@@ -95,7 +95,7 @@ class HaierClimateBase : public esphome::Component,
SENDING_ALARM_STATUS_REQUEST,
NUM_PROTOCOL_PHASES
};
const char *phase_to_string_(ProtocolPhases phase);
const LogString *phase_to_string_(ProtocolPhases phase);
virtual void set_handlers() = 0;
virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
virtual haier_protocol::HaierMessage get_control_message() = 0; // NOLINT(readability-identifier-naming)
+1 -1
View File
@@ -564,7 +564,7 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
default:
// Shouldn't get here
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
LOG_STR_ARG(phase_to_string_(this->protocol_phase_)), (int) this->protocol_phase_);
this->set_phase(ProtocolPhases::SENDING_INIT_1);
break;
}
+6 -5
View File
@@ -9,12 +9,9 @@ from ..climate import (
CONF_HORIZONTAL_AIRFLOW,
CONF_VERTICAL_AIRFLOW,
HonClimate,
haier_ns,
)
CODEOWNERS = ["@paveldn"]
VerticalAirflowSelect = haier_ns.class_("VerticalAirflowSelect", select.Select)
HorizontalAirflowSelect = haier_ns.class_("HorizontalAirflowSelect", select.Select)
# Same order as VERTICAL_SWING_MODES_ORDER and HORIZONTAL_SWING_MODES_ORDER in hon_climate.h
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = [
@@ -39,12 +36,12 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.Optional(CONF_VERTICAL_AIRFLOW): select.select_schema(
VerticalAirflowSelect,
select.ApplySelect,
icon="mdi:arrow-expand-vertical",
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_HORIZONTAL_AIRFLOW): select.select_schema(
HorizontalAirflowSelect,
select.ApplySelect,
icon="mdi:arrow-expand-horizontal",
entity_category=ENTITY_CATEGORY_CONFIG,
),
@@ -59,9 +56,13 @@ async def to_code(config: ConfigType) -> None:
CONF_VERTICAL_AIRFLOW,
hub.set_vertical_airflow_select,
options=AIRFLOW_VERTICAL_DIRECTION_OPTIONS,
apply="set_vertical_airflow("
"haier::airflow_order_at(haier::VERTICAL_SWING_MODES_ORDER, index))",
)
await selects(
CONF_HORIZONTAL_AIRFLOW,
hub.set_horizontal_airflow_select,
options=AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS,
apply="set_horizontal_airflow("
"haier::airflow_order_at(haier::HORIZONTAL_SWING_MODES_ORDER, index))",
)
@@ -1,15 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../hon_climate.h"
namespace esphome::haier {
class HorizontalAirflowSelect : public select::Select, public Parented<HonClimate> {
protected:
void control(size_t index) override {
this->parent_->set_horizontal_airflow(airflow_order_at(HORIZONTAL_SWING_MODES_ORDER, index));
}
};
} // namespace esphome::haier
@@ -1,15 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../hon_climate.h"
namespace esphome::haier {
class VerticalAirflowSelect : public select::Select, public Parented<HonClimate> {
protected:
void control(size_t index) override {
this->parent_->set_vertical_airflow(airflow_order_at(VERTICAL_SWING_MODES_ORDER, index));
}
};
} // namespace esphome::haier
@@ -93,7 +93,7 @@ haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cyc
if (this->protocol_phase_ >= ProtocolPhases::IDLE)
return HaierClimateBase::timeout_default_handler_(message_type);
ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type,
phase_to_string_(this->protocol_phase_));
LOG_STR_ARG(phase_to_string_(this->protocol_phase_)));
ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1);
if (new_phase >= ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST)
new_phase = ProtocolPhases::SENDING_INIT_1;
@@ -217,7 +217,7 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
default:
// Shouldn't get here
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
LOG_STR_ARG(phase_to_string_(this->protocol_phase_)), (int) this->protocol_phase_);
this->set_phase(ProtocolPhases::SENDING_INIT_1);
break;
}
+3 -6
View File
@@ -1,6 +1,7 @@
#include "htu21d.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::htu21d {
@@ -112,11 +113,7 @@ void HTU21DComponent::set_heater(bool status) {
this->status_set_warning();
return;
}
if (status) {
raw_heater |= (1 << HTU21D_REG_HTRE_BIT);
} else {
raw_heater &= ~(1 << HTU21D_REG_HTRE_BIT);
}
assign_bits(raw_heater, 1 << HTU21D_REG_HTRE_BIT, status);
if (this->write_register(HTU21D_WRITERHT_REG_CMD, &raw_heater, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
+1 -4
View File
@@ -40,10 +40,7 @@ RG_MODELS = {
# 1.200 - https://rainsensors.com/wp-content/uploads/sites/3/2022/03/2022.02.17-rev-1.200-rg-9_instructions.pdf
}
RG15_RESOLUTION = {
"low": RG15Resolution.FORCE_LOW,
"high": RG15Resolution.FORCE_HIGH,
}
RG15_RESOLUTION = RG15Resolution.options("low", "high", prefix="FORCE_")
SUPPORTED_OPTIONS = {
CONF_ACC: ["RG_15"],
@@ -270,21 +270,23 @@ void I2SAudioMicrophone::mic_task(void *params) {
{ // Ensures the samples vector is freed when the task stops
const size_t bytes_to_read = this_microphone->audio_stream_info_.ms_to_bytes(READ_DURATION_MS);
// Copied so the format cannot change while the task runs
const audio::AudioStreamInfo stream_info = this_microphone->audio_stream_info_;
const size_t bytes_to_read = stream_info.ms_to_bytes(READ_DURATION_MS);
std::vector<uint8_t> samples;
samples.reserve(bytes_to_read);
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_RUNNING);
while (!(xEventGroupGetBits(this_microphone->event_group_) & MicrophoneEventGroupBits::COMMAND_STOP)) {
if (this_microphone->data_callbacks_.size() > 0) {
if (this_microphone->has_data_callbacks_()) {
samples.resize(bytes_to_read);
size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * READ_DURATION_MS);
samples.resize(bytes_read);
if (this_microphone->correct_dc_offset_) {
this_microphone->fix_dc_offset_(samples);
}
this_microphone->data_callbacks_.call(samples);
this_microphone->publish_audio_(samples, stream_info);
} else {
vTaskDelay(pdMS_TO_TICKS(READ_DURATION_MS));
}
+1 -6
View File
@@ -19,12 +19,7 @@ void ICNT86Touchscreen::setup() {
}
// Perform reset if necessary
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
delay(10);
this->reset_pin_->digital_write(true);
}
pulse_reset(this->reset_pin_, {.low_ms = 10});
if (this->x_raw_max_ == this->x_raw_min_) {
this->x_raw_max_ = this->display_->get_native_width();
+4 -3
View File
@@ -1,8 +1,7 @@
"""Shared base for the infrared and radio_frequency entity components."""
import esphome.codegen as cg
from esphome.components import remote_base
from esphome.components.api import DOMAIN as API_DOMAIN
from esphome.const import CONF_API
from esphome.core import CORE
from esphome.core.entity_helpers import queue_entity_register
from esphome.types import ConfigType
@@ -24,8 +23,10 @@ async def attach_transmitter(var: cg.MockObj, config: ConfigType, key: str) -> N
does. Without this call anywhere in the build a request is answered as
soon as the frame is handed over.
"""
from esphome.components import remote_base
await remote_base.register_transmittable(var, config, key)
if API_DOMAIN in CORE.config:
if CONF_API in CORE.config:
cg.add_define("USE_IR_RF_TRANSMIT_COMPLETE")
@@ -2,11 +2,20 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::kamstrup_kmp {
ESPHOME_LOG_TAG(TAG, "kamstrup_kmp");
// KMP units indexed by unit code; the empty first entry doubles as the fallback for unknown codes
PROGMEM_STRING_TABLE(KmpUnits, "", "Wh", "kWh", "MWh", "GWh", "J", "kJ", "MJ", "GJ", "Cal", "kCal", "Mcal", "Gcal",
"varh", "kvarh", "Mvarh", "Gvarh", "VAh", "kVAh", "MVAh", "GVAh", "kW", "kW", "MW", "GW", "kvar",
"kvar", "Mvar", "Gvar", "VA", "kVA", "MVA", "GVA", "V", "A", "kV", "kA", "C", "K", "l", "m3",
"l/h", "m3/h", "m3xC", "ton", "ton/h", "h", "hh:mm:ss", "yy:mm:dd", "yyyy:mm:dd", "mm:dd", "",
"bar", "RTC", "ASCII", "m3 x 10", "ton x 10", "GJ x 10", "minutes", "Bitfield", "s", "ms", "days",
"RTC-Q", "Datetime");
void KamstrupKMPComponent::dump_config() {
ESP_LOGCONFIG(TAG, "kamstrup_kmp:");
LOG_UPDATE_INTERVAL(this);
@@ -237,8 +246,6 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_
}
void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint8_t unit_idx) {
const char *unit = unit_idx < sizeof(UNITS) / sizeof(UNITS[0]) ? UNITS[unit_idx] : "";
// Standard sensors
if (command == CMD_HEAT_ENERGY && this->heat_energy_sensor_ != nullptr) {
this->heat_energy_sensor_->publish_state(value);
@@ -263,7 +270,8 @@ void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint
}
}
ESP_LOGD(TAG, "Received value for command 0x%04X: %.3f [%s]", command, value, unit);
ESP_LOGD(TAG, "Received value for command 0x%04X: %.3f [%s]", command, value,
LOG_STR_ARG(KmpUnits::get_log_str(unit_idx, 0)));
}
} // namespace esphome::kamstrup_kmp
@@ -63,16 +63,6 @@ static const uint16_t CMD_TEMP_DIFF = 0x0059;
static const uint16_t CMD_FLOW = 0x004A;
static const uint16_t CMD_VOLUME = 0x0044;
// KMP units
static const char *const UNITS[] = {
"", "Wh", "kWh", "MWh", "GWh", "J", "kJ", "MJ", "GJ", "Cal",
"kCal", "Mcal", "Gcal", "varh", "kvarh", "Mvarh", "Gvarh", "VAh", "kVAh", "MVAh",
"GVAh", "kW", "kW", "MW", "GW", "kvar", "kvar", "Mvar", "Gvar", "VA",
"kVA", "MVA", "GVA", "V", "A", "kV", "kA", "C", "K", "l",
"m3", "l/h", "m3/h", "m3xC", "ton", "ton/h", "h", "hh:mm:ss", "yy:mm:dd", "yyyy:mm:dd",
"mm:dd", "", "bar", "RTC", "ASCII", "m3 x 10", "ton x 10", "GJ x 10", "minutes", "Bitfield",
"s", "ms", "days", "RTC-Q", "Datetime"};
class KamstrupKMPComponent final : public PollingComponent, public uart::UARTDevice {
public:
void set_heat_energy_sensor(sensor::Sensor *sensor) { this->heat_energy_sensor_ = sensor; }
+18 -10
View File
@@ -13,11 +13,7 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
BaudRateSelect = ld2410_ns.class_("BaudRateSelect", select.Select)
DistanceResolutionSelect = ld2410_ns.class_("DistanceResolutionSelect", select.Select)
LightOutControlSelect = ld2410_ns.class_("LightOutControlSelect", select.Select)
from .. import CONF_LD2410_ID, LD2410Component
CONF_DISTANCE_RESOLUTION = "distance_resolution"
CONF_LIGHT_FUNCTION = "light_function"
@@ -27,22 +23,22 @@ CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_DISTANCE_RESOLUTION): select.select_schema(
DistanceResolutionSelect,
select.ApplySelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RULER,
),
cv.Optional(CONF_LIGHT_FUNCTION): select.select_schema(
LightOutControlSelect,
select.ApplySelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_LIGHTBULB,
),
cv.Optional(CONF_OUT_PIN_LEVEL): select.select_schema(
LightOutControlSelect,
select.ApplySelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_SCALE,
),
cv.Optional(CONF_BAUD_RATE): select.select_schema(
BaudRateSelect,
select.ApplySelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_THERMOMETER,
),
@@ -57,18 +53,30 @@ async def to_code(config: ConfigType) -> None:
CONF_DISTANCE_RESOLUTION,
hub.set_distance_resolution_select,
options=["0.2m", "0.75m"],
apply=(
"set_distance_resolution(static_cast<ld2410::DistanceResolution>("
"progmem_read_byte(&ld2410::DISTANCE_RESOLUTIONS[index])))"
),
publish="before",
)
light = {"apply": "set_light_out_control()", "publish": "before"}
await selects(
CONF_OUT_PIN_LEVEL, hub.set_out_pin_level_select, options=["low", "high"]
CONF_OUT_PIN_LEVEL,
hub.set_out_pin_level_select,
options=["low", "high"],
**light,
)
await selects(
CONF_LIGHT_FUNCTION,
hub.set_light_function_select,
options=["off", "below", "above"],
**light,
)
await selects(
CONF_BAUD_RATE,
hub.set_baud_rate_select,
apply="set_baud_rate(static_cast<ld2410::BaudRate>(index + 1))",
publish="before",
options=[
"9600",
"19200",
@@ -1,10 +0,0 @@
#include "baud_rate_select.h"
namespace esphome::ld2410 {
void BaudRateSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_baud_rate(static_cast<BaudRate>(index + 1));
}
} // namespace esphome::ld2410
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class BaudRateSelect final : public select::Select, public Parented<LD2410Component> {
public:
BaudRateSelect() = default;
protected:
void control(size_t index) override;
};
} // namespace esphome::ld2410
@@ -1,11 +0,0 @@
#include "distance_resolution_select.h"
namespace esphome::ld2410 {
void DistanceResolutionSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_distance_resolution(
static_cast<DistanceResolution>(progmem_read_byte(&DISTANCE_RESOLUTIONS[index])));
}
} // namespace esphome::ld2410
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class DistanceResolutionSelect final : public select::Select, public Parented<LD2410Component> {
public:
DistanceResolutionSelect() = default;
protected:
void control(size_t index) override;
};
} // namespace esphome::ld2410
@@ -1,10 +0,0 @@
#include "light_out_control_select.h"
namespace esphome::ld2410 {
void LightOutControlSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_light_out_control();
}
} // namespace esphome::ld2410
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class LightOutControlSelect final : public select::Select, public Parented<LD2410Component> {
public:
LightOutControlSelect() = default;
protected:
void control(size_t index) override;
};
} // namespace esphome::ld2410
+13 -8
View File
@@ -13,11 +13,7 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component
BaudRateSelect = LD2412_ns.class_("BaudRateSelect", select.Select)
DistanceResolutionSelect = LD2412_ns.class_("DistanceResolutionSelect", select.Select)
LightOutControlSelect = LD2412_ns.class_("LightOutControlSelect", select.Select)
from .. import CONF_LD2412_ID, LD2412Component
CONF_DISTANCE_RESOLUTION = "distance_resolution"
CONF_LIGHT_FUNCTION = "light_function"
@@ -27,17 +23,17 @@ CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_BAUD_RATE): select.select_schema(
BaudRateSelect,
select.ApplySelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_THERMOMETER,
),
cv.Optional(CONF_DISTANCE_RESOLUTION): select.select_schema(
DistanceResolutionSelect,
select.ApplySelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RULER,
),
cv.Optional(CONF_LIGHT_FUNCTION): select.select_schema(
LightOutControlSelect,
select.ApplySelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_LIGHTBULB,
),
@@ -66,16 +62,25 @@ async def to_code(config: ConfigType) -> None:
"256000",
"460800",
],
apply="set_baud_rate(static_cast<ld2412::BaudRate>(index + 1))",
publish="before",
)
await selects(
CONF_DISTANCE_RESOLUTION,
hub.set_distance_resolution_select,
options=["0.2m", "0.5m", "0.75m"],
apply=(
"set_distance_resolution(static_cast<ld2412::DistanceResolution>("
"progmem_read_byte(&ld2412::DISTANCE_RESOLUTIONS[index])))"
),
publish="before",
)
await selects(
CONF_LIGHT_FUNCTION,
hub.set_light_function_select,
options=["off", "below", "above"],
apply="set_light_out_control()",
publish="before",
)
# The pin level travels in the basic configuration command, not the light control one
await selects(
@@ -1,10 +0,0 @@
#include "baud_rate_select.h"
namespace esphome::ld2412 {
void BaudRateSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_baud_rate(static_cast<BaudRate>(index + 1));
}
} // namespace esphome::ld2412
@@ -1,17 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class BaudRateSelect final : public select::Select, public Parented<LD2412Component> {
public:
// User provided, not "= default": `new(p) BaudRateSelect()` would zero-fill .bss that is already zero.
BaudRateSelect() {}
protected:
void control(size_t index) override;
};
} // namespace esphome::ld2412
@@ -1,11 +0,0 @@
#include "distance_resolution_select.h"
namespace esphome::ld2412 {
void DistanceResolutionSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_distance_resolution(
static_cast<DistanceResolution>(progmem_read_byte(&DISTANCE_RESOLUTIONS[index])));
}
} // namespace esphome::ld2412
@@ -1,17 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class DistanceResolutionSelect final : public select::Select, public Parented<LD2412Component> {
public:
// User provided, not "= default": `new(p) DistanceResolutionSelect()` would zero-fill .bss that is already zero.
DistanceResolutionSelect() {}
protected:
void control(size_t index) override;
};
} // namespace esphome::ld2412
@@ -1,10 +0,0 @@
#include "light_out_control_select.h"
namespace esphome::ld2412 {
void LightOutControlSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_light_out_control();
}
} // namespace esphome::ld2412
@@ -1,17 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class LightOutControlSelect final : public select::Select, public Parented<LD2412Component> {
public:
// User provided, not "= default": `new(p) LightOutControlSelect()` would zero-fill .bss that is already zero.
LightOutControlSelect() {}
protected:
void control(size_t index) override;
};
} // namespace esphome::ld2412
+7 -4
View File
@@ -4,7 +4,7 @@ import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns
from .. import CONF_LD2420_ID, LD2420Component
CONF_OPERATING_MODE = "operating_mode"
CONF_SELECTS = [
@@ -13,12 +13,11 @@ CONF_SELECTS = [
"Simple",
]
LD2420Select = ld2420_ns.class_("LD2420Select", select.Select, cg.Component)
CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component),
cv.Required(CONF_OPERATING_MODE): select.select_schema(
LD2420Select,
select.ApplySelect,
entity_category=ENTITY_CATEGORY_CONFIG,
),
}
@@ -28,5 +27,9 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2420_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_OPERATING_MODE, hub.set_operating_mode_select, options=CONF_SELECTS
CONF_OPERATING_MODE,
hub.set_operating_mode_select,
options=CONF_SELECTS,
apply="set_operating_mode(static_cast<ld2420::OpMode>(index + 1))",
publish="before",
)
@@ -1,14 +0,0 @@
#include "operating_mode_select.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::ld2420 {
ESPHOME_LOG_TAG(TAG, "ld2420.select");
void LD2420Select::control(size_t index) {
this->publish_state(index);
this->parent_->set_operating_mode(static_cast<OpMode>(index + 1));
}
} // namespace esphome::ld2420
@@ -1,16 +0,0 @@
#pragma once
#include "../ld2420.h"
#include "esphome/components/select/select.h"
namespace esphome::ld2420 {
class LD2420Select final : public Component, public select::Select, public Parented<LD2420Component> {
public:
LD2420Select() = default;
protected:
void control(size_t index) override;
};
} // namespace esphome::ld2420
Loaded 100 of 250 files, more files were not shown because too many files have changed in this diff. Show more