mirror of
https://github.com/esphome/esphome.git
synced 2026-10-27 00:12:11 +00:00
Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbecd1cbcb | ||
|
|
a4e7f4c680 | ||
|
|
557ed04f7f | ||
|
|
e08cd5a4f0 | ||
|
|
c04cdea1a0 | ||
|
|
2c11cfd1fa | ||
|
|
46d8c3ec02 | ||
|
|
3259e72d9d | ||
|
|
6cb622417a | ||
|
|
960316abd6 | ||
|
|
29adc6f7e7 | ||
|
|
87341dc136 | ||
|
|
cfe5809654 | ||
|
|
9c54f96520 | ||
|
|
0cc5b372fb | ||
|
|
f7c8cba8b7 | ||
|
|
79a22e9a9a | ||
|
|
103023737d | ||
|
|
6b6b70a8b6 | ||
|
|
d312bf1b73 | ||
|
|
0d2f8c0e88 | ||
|
|
2f8b50c5af | ||
|
|
c4b598be3e | ||
|
|
0cd16df16f | ||
|
|
a680974da2 | ||
|
|
78f0b1ceba | ||
|
|
c5a841e11e | ||
|
|
365965f9bb | ||
|
|
16011d7fce | ||
|
|
4486dc53d8 | ||
|
|
8d5e3ce534 | ||
|
|
9ea4d35a50 | ||
|
|
5cceddfbf4 | ||
|
|
ff6a37a052 | ||
|
|
6990092be0 | ||
|
|
9220c79a51 | ||
|
|
215fcd4f5d | ||
|
|
918e3b3a58 | ||
|
|
347eb3902e | ||
|
|
177820083f | ||
|
|
b0a0524091 |
No files matched your search
@@ -443,7 +443,8 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
- **Hub forwarding entities need no C++ class:** a button, switch, number or select that only calls its hub
|
||||
uses `button.ApplyButton` (`switch.ApplySwitch`, `number.ApplyNumber`, `select.ApplySelect`) in its schema
|
||||
and `apply=` on the `sub_*` call. The body is a call on the hub with `entity` and `state`, `value` or
|
||||
`index`/`option` in scope; on a switch, number or select `publish="before"` or `"after"` publishes the state.
|
||||
`index`/`option` in scope; on a switch, number or select `publish="before"` or `"after"` publishes the state,
|
||||
and `publish="on_success"` publishes only when the hub call returns true (a write the device can refuse).
|
||||
`publish="momentary"` is only for legacy action switches (restart, factory reset): it publishes off and runs
|
||||
the body only when turned on. Never use it for a new entity; an action is a button.
|
||||
```python
|
||||
|
||||
+17
-10
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]() {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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:"); }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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,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
|
||||
@@ -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
|
||||
Loaded 100 of 258 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user