mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 09:36:04 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13dcacfc33 | ||
|
|
76ad7a838e | ||
|
|
1dff22f578 | ||
|
|
c2eb540b37 | ||
|
|
fad21e3f14 | ||
|
|
904f0df920 | ||
|
|
7128d22f07 | ||
|
|
825e2c3dd0 |
No files matched your search
+2
-1
@@ -149,6 +149,7 @@ esphome/components/cst226/* @clydebarrow
|
||||
esphome/components/cst328/* @latonita
|
||||
esphome/components/cst816/* @clydebarrow
|
||||
esphome/components/cst9220/* @clydebarrow
|
||||
esphome/components/cstxxx/* @clydebarrow @latonita
|
||||
esphome/components/ct_clamp/* @jesserockz
|
||||
esphome/components/current_based/* @djwmarcx
|
||||
esphome/components/cw2017/* @exciton
|
||||
@@ -741,7 +742,7 @@ esphome/components/web_server_idf/* @dentra
|
||||
esphome/components/weikai/* @DrCoolZic
|
||||
esphome/components/weikai_i2c/* @DrCoolZic
|
||||
esphome/components/weikai_spi/* @DrCoolZic
|
||||
esphome/components/whirlpool/* @arpena @glmnet
|
||||
esphome/components/whirlpool/* @arpena @glmnet @Rodrigoah
|
||||
esphome/components/whynter/* @aeonsablaze
|
||||
esphome/components/wiegand/* @ssieb
|
||||
esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard
|
||||
|
||||
@@ -1,20 +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,
|
||||
validate_passkey,
|
||||
)
|
||||
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
|
||||
from esphome.components.ble_device_base import validate_passkey
|
||||
from esphome.components.const import (
|
||||
CONF_ACCEPT,
|
||||
CONF_ON_NUMERIC_COMPARISON_REQUEST,
|
||||
@@ -22,10 +13,7 @@ from esphome.components.const import (
|
||||
CONF_ON_PASSKEY_REQUEST,
|
||||
CONF_PASSKEY,
|
||||
)
|
||||
from esphome.config_helpers import (
|
||||
filter_source_files_from_platform,
|
||||
frameworks_for_platforms,
|
||||
)
|
||||
from esphome.components.esp32_ble import BTLoggers
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CHARACTERISTIC_UUID,
|
||||
@@ -38,55 +26,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"
|
||||
|
||||
@@ -122,12 +70,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)
|
||||
)
|
||||
@@ -160,175 +112,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),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -338,26 +177,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]:
|
||||
@@ -368,8 +187,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)),
|
||||
}
|
||||
)
|
||||
@@ -380,34 +199,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(validate_passkey),
|
||||
}
|
||||
),
|
||||
BLE_PASSKEY_REPLY_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
|
||||
cv.Required(CONF_PASSKEY): cv.templatable(validate_passkey),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
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),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -435,8 +245,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]
|
||||
@@ -451,20 +259,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
|
||||
@@ -518,48 +344,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
|
||||
@@ -50,16 +50,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
|
||||
@@ -67,14 +66,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,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation, pins
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
@@ -13,6 +15,7 @@ from esphome.const import (
|
||||
CONF_VALUE,
|
||||
CONF_WAIT_TIME,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@lygris", "@gabest11"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
@@ -22,6 +25,8 @@ MULTI_CONF = True
|
||||
ns = cg.esphome_ns.namespace("cc1101")
|
||||
CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice)
|
||||
CC1101Listener = ns.class_("CC1101Listener")
|
||||
PACKET_TYPE = cg.std_vector.template(cg.uint8)
|
||||
RX_FIFO_SIZE = 64
|
||||
|
||||
# Config keys
|
||||
CONF_RX_ATTENUATION = "rx_attenuation"
|
||||
@@ -241,20 +246,34 @@ CONFIG_MAP = {
|
||||
cv.Optional(CONF_BS_POST_KI): cv.enum(BS_POST_KI, upper=False),
|
||||
cv.Optional(CONF_BS_POST_KP): cv.enum(BS_POST_KP, upper=False),
|
||||
cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t,
|
||||
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_WHITENING, default=False): cv.boolean,
|
||||
}
|
||||
|
||||
|
||||
def _validate_packet_mode(config):
|
||||
if config.get(CONF_PACKET_MODE, False):
|
||||
def _is_long_packet(packet_length: Any) -> bool:
|
||||
"""A packet_length that does not fit the RX FIFO, or is a lambda."""
|
||||
return cg.is_template(packet_length) or (packet_length or 0) > RX_FIFO_SIZE
|
||||
|
||||
|
||||
def _validate_packet_mode(config: ConfigType) -> ConfigType:
|
||||
packet_mode = config.get(CONF_PACKET_MODE, False)
|
||||
packet_length = config.get(CONF_PACKET_LENGTH)
|
||||
if packet_mode:
|
||||
if CONF_GDO0_PIN not in config:
|
||||
raise cv.Invalid("gdo0_pin is required when packet_mode is enabled")
|
||||
if CONF_PACKET_LENGTH not in config:
|
||||
if packet_length is None:
|
||||
raise cv.Invalid("packet_length is required when packet_mode is enabled")
|
||||
if config[CONF_PACKET_LENGTH] > 64:
|
||||
raise cv.Invalid("packet_length must be <= 64 (FIFO size)")
|
||||
elif cg.is_template(packet_length):
|
||||
raise cv.Invalid("packet_mode is required when packet_length is a lambda")
|
||||
if (
|
||||
packet_mode
|
||||
and _is_long_packet(packet_length)
|
||||
and config.get(CONF_CRC_ENABLE, False)
|
||||
):
|
||||
raise cv.Invalid(
|
||||
"crc_enable is not supported with packet_length > 64 or a lambda"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -263,6 +282,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CC1101Component),
|
||||
cv.Optional(CONF_GDO0_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_PACKET_LENGTH): cv.templatable(
|
||||
cv.int_range(min=0, max=1024)
|
||||
),
|
||||
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
|
||||
}
|
||||
)
|
||||
@@ -282,11 +304,24 @@ async def to_code(config):
|
||||
cg.add(getattr(var, f"set_{key}")(config[key]))
|
||||
|
||||
await cg.add_pin(var.set_gdo0_pin, config.get(CONF_GDO0_PIN))
|
||||
if (packet_length := config.get(CONF_PACKET_LENGTH)) is not None:
|
||||
# packet_mode can also be switched on at run time, so this does not check it
|
||||
if _is_long_packet(packet_length):
|
||||
cg.add_define("USE_CC1101_LONG_PACKET")
|
||||
if cg.is_template(packet_length):
|
||||
lambda_ = await cg.process_lambda(
|
||||
packet_length,
|
||||
[(PACKET_TYPE.operator("ref").operator("const"), "x")],
|
||||
return_type=cg.int32,
|
||||
)
|
||||
cg.add(var.set_packet_length_lambda(lambda_))
|
||||
else:
|
||||
cg.add(var.set_packet_length(packet_length))
|
||||
if CONF_ON_PACKET in config:
|
||||
await automation.build_automation(
|
||||
var.get_packet_trigger(),
|
||||
[
|
||||
(cg.std_vector.template(cg.uint8), "x"),
|
||||
(PACKET_TYPE, "x"),
|
||||
(cg.float_, "freq_offset"),
|
||||
(cg.float_, "rssi"),
|
||||
(cg.uint8, "lqi"),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "cc1101.h"
|
||||
#include "cc1101pa.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
@@ -14,9 +16,15 @@ namespace esphome::cc1101 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cc1101");
|
||||
static constexpr size_t TX_FIFO_SIZE = 64;
|
||||
static constexpr size_t RX_FIFO_SIZE = 64;
|
||||
// RX FIFO threshold for FIFO_THR = 7 in long packet mode
|
||||
static constexpr uint8_t RX_FIFO_THRESHOLD = 32;
|
||||
// The crystal takes about 150 us to start after CSn goes low
|
||||
static constexpr uint32_t WAKEUP_DELAY_US = 200;
|
||||
|
||||
static float freq_offset_hz(uint8_t freqest) { return static_cast<int8_t>(freqest) * (XTAL_FREQUENCY / (1 << 14)); }
|
||||
static float rssi_dbm(int8_t rssi) { return (rssi * RSSI_STEP) - RSSI_OFFSET; }
|
||||
|
||||
// MDMCFG2 MOD_FORMAT values
|
||||
PROGMEM_STRING_TABLE(ModulationStrings, "2-FSK", "GFSK", "UNUSED", "ASK/OOK", "4-FSK", "UNUSED", "UNUSED", "MSK");
|
||||
|
||||
@@ -114,8 +122,6 @@ CC1101Component::CC1101Component() {
|
||||
memset(this->pa_table_, 0, sizeof(this->pa_table_));
|
||||
}
|
||||
|
||||
void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void CC1101Component::setup() {
|
||||
this->spi_setup();
|
||||
|
||||
@@ -193,7 +199,220 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
|
||||
this->packet_trigger_.trigger(packet, freq_offset, rssi, lqi);
|
||||
}
|
||||
|
||||
float CC1101Component::symbol_rate_() const {
|
||||
return (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
|
||||
}
|
||||
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
bool CC1101Component::is_long_packet_mode_() const {
|
||||
return this->packet_mode_ && (this->packet_length_ > RX_FIFO_SIZE || this->packet_length_func_ != nullptr);
|
||||
}
|
||||
|
||||
void CC1101Component::arm_length_(uint16_t length) {
|
||||
// Over 255 bytes or unknown starts in infinite mode (datasheet section 15.2.2). PKTLEN must not be 0, so a
|
||||
// multiple of 256 stays in infinite mode and ends on the MCU byte count alone.
|
||||
this->packet_expected_length_ = length;
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(length > 0 && length < 256 ? LengthConfig::LENGTH_CONFIG_FIXED
|
||||
: LengthConfig::LENGTH_CONFIG_INFINITE);
|
||||
this->state_.PKTLEN = length % 256 == 0 ? 0xFF : static_cast<uint8_t>(length % 256);
|
||||
}
|
||||
|
||||
void CC1101Component::reset_long_packet_rx_() {
|
||||
this->packet_.clear();
|
||||
this->packet_last_byte_ms_ = 0;
|
||||
this->high_freq_.stop();
|
||||
this->arm_length_(this->packet_length_func_ != nullptr ? 0 : this->packet_length_);
|
||||
}
|
||||
|
||||
void CC1101Component::restart_long_packet_rx_() {
|
||||
this->enter_idle_();
|
||||
this->enter_rx_();
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void CC1101Component::maybe_switch_long_packet_to_fixed_() {
|
||||
const uint16_t expected = this->packet_expected_length_;
|
||||
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED) || expected == 0 ||
|
||||
expected % 256 == 0) {
|
||||
return;
|
||||
}
|
||||
const uint16_t received = this->packet_.size();
|
||||
if (expected - received >= 256) {
|
||||
return;
|
||||
}
|
||||
// PKTLEN keeps the (length % 256) set at the start, as in TI Design Note DN500 section 2.3
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
|
||||
this->write_(Register::PKTCTRL0);
|
||||
ESP_LOGVV(TAG, "Switched long packet RX to fixed mode: received=%u expected=%u pktlen=%u", received, expected,
|
||||
this->state_.PKTLEN);
|
||||
}
|
||||
|
||||
void CC1101Component::finish_long_packet_() {
|
||||
this->packet_.resize(this->packet_expected_length_);
|
||||
this->read_(Register::FREQEST);
|
||||
this->read_(Register::LQI);
|
||||
const float freq_offset = freq_offset_hz(this->state_.FREQEST);
|
||||
const uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
|
||||
// No CRC check: crc_enable is rejected for long packets, CRC_OK is only set after the last payload byte.
|
||||
// Restart RX before dispatching so a callback may change settings; swapping back keeps the reserved capacity
|
||||
// unless a callback reserved a larger buffer.
|
||||
std::vector<uint8_t> packet;
|
||||
packet.swap(this->packet_);
|
||||
this->restart_long_packet_rx_();
|
||||
this->call_listeners_(packet, freq_offset, rssi_dbm(this->long_packet_rssi_), lqi);
|
||||
if (packet.capacity() > this->packet_.capacity()) {
|
||||
packet.clear();
|
||||
this->packet_.swap(packet);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::handle_long_packet_(uint8_t rx_bytes) {
|
||||
// Leave one byte in the FIFO unless it holds the rest of the packet (datasheet section 20)
|
||||
uint16_t to_read = rx_bytes > 0 ? rx_bytes - 1 : 0;
|
||||
if (this->packet_expected_length_ > 0) {
|
||||
const uint16_t remaining = this->packet_expected_length_ - this->packet_.size();
|
||||
if (remaining <= rx_bytes) {
|
||||
to_read = remaining;
|
||||
}
|
||||
}
|
||||
|
||||
if (to_read == 0) {
|
||||
if (this->packet_.empty()) {
|
||||
return;
|
||||
}
|
||||
// Discard a packet when no byte arrived for the air time of PACKET_IDLE_TIMEOUT_BYTES
|
||||
const uint32_t timeout_ms =
|
||||
std::max(static_cast<uint32_t>(std::ceil((8.0f * PACKET_IDLE_TIMEOUT_BYTES * 1000.0f) / this->symbol_rate_())),
|
||||
PACKET_IDLE_TIMEOUT_MIN_MS);
|
||||
if (App.get_loop_component_start_time() - this->packet_last_byte_ms_ > timeout_ms) {
|
||||
ESP_LOGW(TAG, "Packet receive timeout, discarding packet: received=%u expected=%u fifo=%u",
|
||||
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_, rx_bytes);
|
||||
this->restart_long_packet_rx_();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t old_size = this->packet_.size();
|
||||
if (old_size == 0) {
|
||||
// The RSSI register follows the live signal; sample it while the packet is still on air
|
||||
this->read_(Register::RSSI);
|
||||
this->long_packet_rssi_ = this->state_.RSSI;
|
||||
// The FIFO threshold signal does not fire for the bytes after the last threshold crossing; poll until done
|
||||
this->high_freq_.start();
|
||||
}
|
||||
// Read through an aligned buffer: for an unaligned target the ESP32 SPI driver allocates a bounce buffer
|
||||
alignas(4) uint8_t staged[RX_FIFO_SIZE];
|
||||
this->read_(Register::FIFO, staged, to_read);
|
||||
this->packet_.resize(old_size + to_read);
|
||||
std::memcpy(this->packet_.data() + old_size, staged, to_read);
|
||||
this->packet_last_byte_ms_ = App.get_loop_component_start_time();
|
||||
ESP_LOGVV(TAG, "Drained RX FIFO: read=%u received=%u expected=%u fifo=%u", to_read,
|
||||
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_, rx_bytes);
|
||||
|
||||
if (this->packet_expected_length_ == 0 && this->packet_length_func_ != nullptr) {
|
||||
const int32_t length = this->packet_length_func_(this->packet_);
|
||||
if (length < 0) {
|
||||
ESP_LOGVV(TAG, "Packet length lambda discarded packet: received=%u", static_cast<uint16_t>(this->packet_.size()));
|
||||
this->restart_long_packet_rx_();
|
||||
return;
|
||||
}
|
||||
if (length > MAX_PACKET_LENGTH) {
|
||||
ESP_LOGW(TAG, "Packet length exceeds maximum, discarding packet: length=%" PRId32 " max=%u", length,
|
||||
MAX_PACKET_LENGTH);
|
||||
this->restart_long_packet_rx_();
|
||||
return;
|
||||
}
|
||||
if (length > 0) {
|
||||
this->arm_length_(static_cast<uint16_t>(length));
|
||||
// A packet shorter than the first FIFO drain is already complete; finish_long_packet_() truncates it
|
||||
if (this->packet_expected_length_ <= this->packet_.size()) {
|
||||
this->finish_long_packet_();
|
||||
return;
|
||||
}
|
||||
this->write_(Register::PKTLEN);
|
||||
this->write_(Register::PKTCTRL0);
|
||||
ESP_LOGVV(TAG, "Resolved packet length: received=%u expected=%u pktlen=%u",
|
||||
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_, this->state_.PKTLEN);
|
||||
}
|
||||
}
|
||||
|
||||
// Only reachable while the length is still unknown; a known length caps every read
|
||||
if (this->packet_.size() > MAX_PACKET_LENGTH) {
|
||||
ESP_LOGW(TAG, "Packet length exceeds maximum, discarding packet: length=%u max=%u",
|
||||
static_cast<uint16_t>(this->packet_.size()), MAX_PACKET_LENGTH);
|
||||
this->restart_long_packet_rx_();
|
||||
return;
|
||||
}
|
||||
|
||||
this->maybe_switch_long_packet_to_fixed_();
|
||||
|
||||
if (this->packet_expected_length_ > 0 && this->packet_.size() >= this->packet_expected_length_) {
|
||||
this->finish_long_packet_();
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::loop_long_packet_() {
|
||||
if (this->sleeping_ || this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) ||
|
||||
this->gdo0_pin_ == nullptr) {
|
||||
this->high_freq_.stop();
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
// While GDO0 (RX FIFO threshold) is high, the FIFO holds at least RX_FIFO_THRESHOLD bytes, so drain without
|
||||
// reading RXBYTES. At high data rates RXBYTES changes between two reads, so waiting for two reads that agree
|
||||
// and retrying on the next loop pass lets the FIFO overflow. MARCSTATE does not change during RX.
|
||||
for (uint8_t i = 0; i < RX_FIFO_SIZE / RX_FIFO_THRESHOLD + 2 && this->gdo0_pin_->digital_read(); i++) {
|
||||
this->read_(Register::MARCSTATE);
|
||||
if (static_cast<State>(this->state_.MARC_STATE) == State::RXFIFO_OVERFLOW) {
|
||||
ESP_LOGW(TAG, "RX FIFO overflow, discarding packet: received=%u expected=%u",
|
||||
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_);
|
||||
this->restart_long_packet_rx_();
|
||||
return;
|
||||
}
|
||||
this->handle_long_packet_(RX_FIFO_THRESHOLD);
|
||||
if (this->packet_.empty()) {
|
||||
return; // finished or discarded
|
||||
}
|
||||
}
|
||||
// Below the threshold the next crossing raises GDO0 again, and after the packet the count no longer changes,
|
||||
// so retrying on the next loop pass is safe here. RXBYTES can be misread while bytes arrive (TI SWRZ020), so
|
||||
// only trust two consecutive reads that agree.
|
||||
this->read_(Register::RXBYTES);
|
||||
uint8_t rx_bytes = this->state_.RXBYTES;
|
||||
bool stable = false;
|
||||
for (uint8_t retries = RXBYTES_STABLE_RETRIES; retries > 0 && !stable; retries--) {
|
||||
const uint8_t previous = rx_bytes;
|
||||
this->read_(Register::RXBYTES);
|
||||
rx_bytes = this->state_.RXBYTES;
|
||||
stable = rx_bytes == previous;
|
||||
}
|
||||
if (!stable) {
|
||||
return;
|
||||
}
|
||||
rx_bytes = this->state_.NUM_RXBYTES;
|
||||
// More than the FIFO holds can only be a misread; the staged drain buffer is FIFO sized
|
||||
if (this->state_.RXFIFO_OVERFLOW || rx_bytes > RX_FIFO_SIZE) {
|
||||
ESP_LOGW(TAG, "RX FIFO overflow, discarding packet: received=%u expected=%u fifo=%u",
|
||||
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_, rx_bytes);
|
||||
this->restart_long_packet_rx_();
|
||||
return;
|
||||
}
|
||||
if (rx_bytes == 0 && this->packet_.empty() && !this->gdo0_pin_->digital_read()) {
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
this->handle_long_packet_(rx_bytes);
|
||||
}
|
||||
#endif
|
||||
|
||||
void CC1101Component::loop() {
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
if (this->is_long_packet_mode_()) {
|
||||
this->loop_long_packet_();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->disable_loop();
|
||||
if (this->sleeping_ || this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) ||
|
||||
this->gdo0_pin_ == nullptr || !this->gdo0_pin_->digital_read()) {
|
||||
@@ -221,7 +440,7 @@ void CC1101Component::loop() {
|
||||
payload_length = this->state_.PKTLEN;
|
||||
expected_rx = payload_length;
|
||||
}
|
||||
if (payload_length == 0 || payload_length > 64 || rx_bytes != expected_rx) {
|
||||
if (payload_length == 0 || payload_length > RX_FIFO_SIZE || rx_bytes != expected_rx) {
|
||||
ESP_LOGW(TAG, "Invalid packet: rx_bytes %u, payload_length %u", rx_bytes, payload_length);
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
@@ -235,8 +454,8 @@ void CC1101Component::loop() {
|
||||
this->read_(Register::FREQEST);
|
||||
this->read_(Register::RSSI);
|
||||
this->read_(Register::LQI);
|
||||
float freq_offset = static_cast<int8_t>(this->state_.FREQEST) * (XTAL_FREQUENCY / (1 << 14));
|
||||
float rssi = (this->state_.RSSI * RSSI_STEP) - RSSI_OFFSET;
|
||||
float freq_offset = freq_offset_hz(this->state_.FREQEST);
|
||||
float rssi = rssi_dbm(this->state_.RSSI);
|
||||
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
|
||||
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
|
||||
if (this->state_.CRC_EN == 0 || crc_ok) {
|
||||
@@ -256,7 +475,7 @@ void CC1101Component::loop() {
|
||||
void CC1101Component::dump_config() {
|
||||
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;
|
||||
float symbol_rate = this->symbol_rate_();
|
||||
float bw = XTAL_FREQUENCY / (8.0f * (4 + this->state_.CHANBW_M) * (1 << this->state_.CHANBW_E));
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"CC1101:\n"
|
||||
@@ -271,6 +490,20 @@ void CC1101Component::dump_config() {
|
||||
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_);
|
||||
LOG_PIN(" GDO0 Pin: ", this->gdo0_pin_);
|
||||
|
||||
if (this->packet_mode_) {
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
if (this->packet_length_func_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Packet Length: lambda");
|
||||
} else
|
||||
#endif
|
||||
if (this->packet_length_ == 0) {
|
||||
ESP_LOGCONFIG(TAG, " Packet Length: variable");
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Packet Length: %u", this->packet_length_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::begin_tx() {
|
||||
@@ -316,6 +549,14 @@ void CC1101Component::reset() {
|
||||
void CC1101Component::set_idle() {
|
||||
ESP_LOGV(TAG, "Setting IDLE state");
|
||||
this->enter_idle_();
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
if (this->is_long_packet_mode_()) {
|
||||
// Drop a partial packet so a pending loop pass cannot finish it and re-enter RX
|
||||
this->strobe_(Command::FRX);
|
||||
this->reset_long_packet_rx_();
|
||||
this->disable_loop();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void CC1101Component::power_down() {
|
||||
@@ -393,7 +634,18 @@ void CC1101Component::enter_idle_() {
|
||||
}
|
||||
}
|
||||
|
||||
bool CC1101Component::enter_rx_() { return this->enter_calibrated_(State::RX, Command::RX); }
|
||||
bool CC1101Component::enter_rx_() {
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
if (this->is_long_packet_mode_()) {
|
||||
// Every RX entry starts a fresh long packet; callers leave the chip in IDLE, where SFRX is allowed
|
||||
this->strobe_(Command::FRX);
|
||||
this->reset_long_packet_rx_();
|
||||
this->write_(Register::PKTLEN);
|
||||
this->write_(Register::PKTCTRL0);
|
||||
}
|
||||
#endif
|
||||
return this->enter_calibrated_(State::RX, Command::RX);
|
||||
}
|
||||
|
||||
bool CC1101Component::enter_tx_() { return this->enter_calibrated_(State::TX, Command::TX); }
|
||||
|
||||
@@ -456,12 +708,22 @@ void CC1101Component::read_(Register reg, uint8_t *buffer, size_t length) {
|
||||
|
||||
CC1101Error CC1101Component::transmit_packet(const uint8_t *data, size_t len) {
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
ESP_LOGW(TAG, "Cannot transmit packet: packet mode is not FIFO");
|
||||
return CC1101Error::PARAMS;
|
||||
}
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
// Long packet TX needs chunked FIFO refills and is not implemented
|
||||
if (this->is_long_packet_mode_()) {
|
||||
ESP_LOGW(TAG, "Cannot transmit packet: long packet mode is RX-only");
|
||||
return CC1101Error::PARAMS;
|
||||
}
|
||||
#endif
|
||||
const bool variable = this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
|
||||
// In variable length mode the FIFO also holds the length byte
|
||||
if (len + (variable ? 1 : 0) > TX_FIFO_SIZE) {
|
||||
ESP_LOGE(TAG, "Packet of %u bytes does not fit the TX FIFO", static_cast<unsigned>(len));
|
||||
ESP_LOGW(TAG,
|
||||
"Cannot transmit packet: long packet TX is not implemented, payload length %u exceeds TX FIFO limit %u",
|
||||
static_cast<unsigned>(len), static_cast<unsigned>(TX_FIFO_SIZE));
|
||||
return CC1101Error::PARAMS;
|
||||
}
|
||||
|
||||
@@ -834,19 +1096,60 @@ void CC1101Component::set_bs_post_kp(BsPostKp value) {
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_packet_mode(bool value) {
|
||||
this->state_.PKT_FORMAT =
|
||||
static_cast<uint8_t>(value ? PacketFormat::PACKET_FORMAT_FIFO : PacketFormat::PACKET_FORMAT_ASYNC_SERIAL);
|
||||
if (value) {
|
||||
// Configure GDO0 for FIFO status (asserts on RX FIFO threshold or end of packet)
|
||||
this->state_.GDO0_CFG = 0x01;
|
||||
// Set max RX FIFO threshold to ensure we only trigger on end-of-packet
|
||||
this->state_.FIFO_THR = 15;
|
||||
// Don't append status bytes to FIFO - we read from registers instead
|
||||
this->state_.APPEND_STATUS = 0;
|
||||
} else {
|
||||
// Configure GDO0 for serial data (async serial mode)
|
||||
void CC1101Component::configure_packet_mode_() {
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
// Ends a long reception in progress, also when switching to a short packet mode
|
||||
this->high_freq_.stop();
|
||||
this->packet_.clear();
|
||||
#endif
|
||||
if (!this->packet_mode_) {
|
||||
this->state_.PKT_FORMAT = static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_ASYNC_SERIAL);
|
||||
this->state_.GDO0_CFG = 0x0D;
|
||||
} else {
|
||||
this->state_.PKT_FORMAT = static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO);
|
||||
this->state_.APPEND_STATUS = 0;
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
if (this->is_long_packet_mode_()) {
|
||||
// Threshold-only signal, reasserts after each partial FIFO drain
|
||||
this->state_.GDO0_CFG = 0x00;
|
||||
this->state_.FIFO_THR = 7;
|
||||
// Reserve once so receiving never allocates; a lambda may read one FIFO drain past MAX_PACKET_LENGTH
|
||||
this->packet_.reserve(this->packet_length_func_ != nullptr ? MAX_PACKET_LENGTH + RX_FIFO_SIZE
|
||||
: this->packet_length_);
|
||||
this->reset_long_packet_rx_();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->state_.GDO0_CFG = 0x01;
|
||||
this->state_.FIFO_THR = 15;
|
||||
if (this->packet_length_ == 0) {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
|
||||
} else {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
|
||||
this->state_.PKTLEN = static_cast<uint8_t>(this->packet_length_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTCTRL1);
|
||||
this->write_(Register::PKTLEN);
|
||||
this->write_(Register::IOCFG0);
|
||||
this->write_(Register::FIFOTHR);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_packet_mode(bool value) {
|
||||
this->packet_mode_ = value;
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
// Long packet RX must drop the FIFO along with a partial packet, so go through IDLE and restart RX
|
||||
if (this->initialized_ && this->is_long_packet_mode_()) {
|
||||
this->reconfigure_packet_length_();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->configure_packet_mode_();
|
||||
}
|
||||
if (this->initialized_) {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
@@ -856,24 +1159,41 @@ void CC1101Component::set_packet_mode(bool value) {
|
||||
this->gdo0_pin_->detach_interrupt();
|
||||
}
|
||||
}
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTCTRL1);
|
||||
this->write_(Register::IOCFG0);
|
||||
this->write_(Register::FIFOTHR);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_packet_length(uint8_t value) {
|
||||
if (value == 0) {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
|
||||
} else {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
|
||||
this->state_.PKTLEN = value;
|
||||
void CC1101Component::set_packet_length(uint16_t value) {
|
||||
#ifndef USE_CC1101_LONG_PACKET
|
||||
if (value > RX_FIFO_SIZE) {
|
||||
ESP_LOGW(TAG, "Packet length %u needs a configured packet_length above %u", value,
|
||||
static_cast<unsigned>(RX_FIFO_SIZE));
|
||||
return;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTLEN);
|
||||
#else
|
||||
this->packet_length_func_ = nullptr;
|
||||
#endif
|
||||
this->packet_length_ = value;
|
||||
this->reconfigure_packet_length_();
|
||||
}
|
||||
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
void CC1101Component::set_packet_length_lambda(packet_length_func_t func) {
|
||||
this->packet_length_func_ = func;
|
||||
this->reconfigure_packet_length_();
|
||||
}
|
||||
#endif
|
||||
|
||||
void CC1101Component::reconfigure_packet_length_() {
|
||||
// While asleep the writes wait for wake_up() and the radio stays asleep, like set_frequency()
|
||||
if (!this->initialized_ || this->sleeping_) {
|
||||
this->configure_packet_mode_();
|
||||
return;
|
||||
}
|
||||
// Like the other runtime setters: go through IDLE, and flush bytes of a reception in progress
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->configure_packet_mode_();
|
||||
this->enter_rx_();
|
||||
}
|
||||
|
||||
void CC1101Component::set_crc_enable(bool value) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "cc1101defs.h"
|
||||
@@ -10,6 +11,7 @@
|
||||
namespace esphome::cc1101 {
|
||||
|
||||
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK };
|
||||
using packet_length_func_t = int32_t (*)(const std::vector<uint8_t> &);
|
||||
|
||||
class CC1101Listener {
|
||||
public:
|
||||
@@ -86,7 +88,10 @@ class CC1101Component final : public Component,
|
||||
|
||||
// Packet mode settings
|
||||
void set_packet_mode(bool value);
|
||||
void set_packet_length(uint8_t value);
|
||||
void set_packet_length(uint16_t value);
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
void set_packet_length_lambda(packet_length_func_t func);
|
||||
#endif
|
||||
void set_crc_enable(bool value);
|
||||
void set_whitening(bool value);
|
||||
|
||||
@@ -101,6 +106,7 @@ class CC1101Component final : public Component,
|
||||
protected:
|
||||
uint16_t chip_id_{0};
|
||||
bool initialized_{false};
|
||||
bool packet_mode_{false};
|
||||
bool sleeping_{false};
|
||||
bool registers_dirty_{false};
|
||||
|
||||
@@ -112,13 +118,38 @@ class CC1101Component final : public Component,
|
||||
|
||||
// GDO pin for packet reception
|
||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||
static void gpio_intr(CC1101Component *arg);
|
||||
// Inline like App.get_loop_component_start_time(): on RP2040 an out-of-line IRAM_ATTR function in the same
|
||||
// file as that inline one fails with a section type conflict
|
||||
static void IRAM_ATTR gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
// Packet handling
|
||||
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
|
||||
void configure_packet_mode_();
|
||||
void reconfigure_packet_length_();
|
||||
float symbol_rate_() const;
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
bool is_long_packet_mode_() const;
|
||||
void arm_length_(uint16_t length);
|
||||
void reset_long_packet_rx_();
|
||||
void restart_long_packet_rx_();
|
||||
void loop_long_packet_();
|
||||
void handle_long_packet_(uint8_t rx_bytes);
|
||||
void finish_long_packet_();
|
||||
void maybe_switch_long_packet_to_fixed_();
|
||||
#endif
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> packet_trigger_;
|
||||
std::vector<uint8_t> packet_;
|
||||
std::vector<CC1101Listener *> listeners_;
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
packet_length_func_t packet_length_func_{nullptr};
|
||||
uint32_t packet_last_byte_ms_{0};
|
||||
#endif
|
||||
uint16_t packet_length_{0};
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
uint16_t packet_expected_length_{0};
|
||||
int8_t long_packet_rssi_{0}; // raw RSSI register
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
#endif
|
||||
|
||||
// Low-level Helpers
|
||||
uint8_t strobe_(Command cmd);
|
||||
|
||||
@@ -15,6 +15,11 @@ static constexpr uint8_t PLL_LOCK_RETRIES = 3;
|
||||
static constexpr uint8_t STATUS_CRC_OK_MASK = 0x80;
|
||||
static constexpr uint8_t STATUS_LQI_MASK = 0x7F;
|
||||
|
||||
static constexpr uint16_t MAX_PACKET_LENGTH = 1024;
|
||||
static constexpr uint16_t PACKET_IDLE_TIMEOUT_BYTES = 64;
|
||||
static constexpr uint32_t PACKET_IDLE_TIMEOUT_MIN_MS = 5;
|
||||
static constexpr uint8_t RXBYTES_STABLE_RETRIES = 4;
|
||||
|
||||
static constexpr uint8_t BUS_BURST = 0x40;
|
||||
static constexpr uint8_t BUS_READ = 0x80;
|
||||
static constexpr uint8_t BUS_WRITE = 0x00;
|
||||
|
||||
@@ -1,36 +1,15 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
|
||||
from esphome.components import cstxxx
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import cst226_ns
|
||||
|
||||
CST226Touchscreen = cst226_ns.class_(
|
||||
"CST226Touchscreen",
|
||||
touchscreen.Touchscreen,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
AUTO_LOAD = ["cstxxx"]
|
||||
|
||||
CST226Touchscreen = cst226_ns.class_("CST226Touchscreen", cstxxx.CSTTouchscreen)
|
||||
|
||||
CST226ButtonListener = cst226_ns.class_("CST226ButtonListener")
|
||||
CONFIG_SCHEMA = (
|
||||
touchscreen.touchscreen_schema("100ms")
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CST226Touchscreen),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x5A))
|
||||
)
|
||||
CONFIG_SCHEMA = cstxxx.cst_touchscreen_schema(CST226Touchscreen, 0x5A)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
|
||||
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
|
||||
await cstxxx.new_cst_touchscreen(config)
|
||||
@@ -14,6 +14,10 @@ void CST226Touchscreen::setup() {
|
||||
}
|
||||
|
||||
void CST226Touchscreen::update_touches() {
|
||||
if (!this->setup_complete_) {
|
||||
this->skip_update_ = true;
|
||||
return;
|
||||
}
|
||||
uint8_t data[28];
|
||||
if (!this->status_comm_result(this->read_bytes(CST226_REG_STATUS, data, sizeof data))) {
|
||||
this->skip_update_ = true;
|
||||
@@ -24,56 +28,39 @@ void CST226Touchscreen::update_touches() {
|
||||
return;
|
||||
}
|
||||
this->update_button_state_(false);
|
||||
if (data[6] != 0xAB || data[0] == 0xAB || data[5] == 0x80) {
|
||||
if (data[cstxxx::SYNC_IDX] != cstxxx::SYNC_BYTE || data[0] == cstxxx::SYNC_BYTE ||
|
||||
data[cstxxx::FINGER_COUNT_IDX] == 0x80) {
|
||||
this->skip_update_ = true;
|
||||
return;
|
||||
}
|
||||
uint8_t num_of_touches = data[5] & 0x7F;
|
||||
uint8_t num_of_touches = data[cstxxx::FINGER_COUNT_IDX] & 0x7F;
|
||||
if (num_of_touches == 0 || num_of_touches > 5) {
|
||||
this->write_byte(0, 0xAB);
|
||||
this->write_byte(CST226_REG_STATUS, cstxxx::SYNC_BYTE);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t index = 0;
|
||||
for (uint8_t i = 0; i != num_of_touches; i++) {
|
||||
uint8_t id = data[index] >> 4;
|
||||
int16_t x = (data[index + 1] << 4) | ((data[index + 3] >> 4) & 0x0F);
|
||||
int16_t y = (data[index + 2] << 4) | (data[index + 3] & 0x0F);
|
||||
int16_t z = data[index + 4];
|
||||
this->add_raw_touch_position_(id, x, y, z);
|
||||
ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
|
||||
index += 5;
|
||||
if (i == 0)
|
||||
index += 2;
|
||||
auto touch = cstxxx::decode_touch(data, i);
|
||||
this->add_raw_touch_position_(touch.id, touch.x, touch.y, touch.z);
|
||||
ESP_LOGV(TAG, "Read touch %d: %d/%d", touch.id, touch.x, touch.y);
|
||||
}
|
||||
}
|
||||
|
||||
bool CST226Touchscreen::read16_(uint16_t addr, uint8_t *data, size_t len) {
|
||||
if (this->read_register16(addr, data, len) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Read data from 0x%04X failed", addr);
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void CST226Touchscreen::continue_setup_() {
|
||||
uint8_t buffer[8];
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
if (!this->setup_check(this->write_byte_reg16(0xD1, 0xD1), LOG_STR("Write byte to 0xD1 failed")))
|
||||
this->setup_interrupt_pin_(gpio::INTERRUPT_FALLING_EDGE);
|
||||
if (!this->enter_command_mode_())
|
||||
return;
|
||||
delay(10);
|
||||
if (this->read16_(0xD204, buffer, 4)) {
|
||||
uint16_t chip_id = buffer[2] + (buffer[3] << 8);
|
||||
uint16_t project_id = buffer[0] + (buffer[1] << 8);
|
||||
ESP_LOGCONFIG(TAG, "Chip ID %X, project ID %x", chip_id, project_id);
|
||||
}
|
||||
if (!this->check_firmware_())
|
||||
return;
|
||||
if (!this->setup_check(this->read_words_(cstxxx::REG_CHIP_INFO, this->project_id_, this->chip_id_),
|
||||
LOG_STR("Failed to read chip ID")))
|
||||
return;
|
||||
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
|
||||
if (this->read16_(0xD1F8, buffer, 4)) {
|
||||
this->x_raw_max_ = buffer[0] + (buffer[1] << 8);
|
||||
this->y_raw_max_ = buffer[2] + (buffer[3] << 8);
|
||||
uint16_t x_max, y_max;
|
||||
if (this->read_words_(cstxxx::REG_RESOLUTION, x_max, y_max)) {
|
||||
this->x_raw_max_ = x_max;
|
||||
this->y_raw_max_ = y_max;
|
||||
if (this->swap_x_y_)
|
||||
std::swap(this->x_raw_max_, this->y_raw_max_);
|
||||
} else {
|
||||
@@ -81,8 +68,10 @@ void CST226Touchscreen::continue_setup_() {
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
}
|
||||
this->status_comm_check(this->write_register16(cstxxx::REG_NORMAL_MODE, nullptr, 0) == i2c::ERROR_OK);
|
||||
this->setup_complete_ = true;
|
||||
}
|
||||
|
||||
void CST226Touchscreen::update_button_state_(bool state) {
|
||||
if (this->button_touched_ == state)
|
||||
return;
|
||||
@@ -96,6 +85,10 @@ void CST226Touchscreen::dump_config() {
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Chip ID: 0x%04X, Project ID: 0x%04X\n"
|
||||
" X/Y raw max: %d/%d",
|
||||
this->chip_id_, this->project_id_, this->x_raw_max_, this->y_raw_max_);
|
||||
}
|
||||
|
||||
} // namespace esphome::cst226
|
||||
@@ -1,11 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/components/cstxxx/cstxxx.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::cst226 {
|
||||
|
||||
static const uint8_t CST226_REG_STATUS = 0x00;
|
||||
@@ -15,28 +16,23 @@ class CST226ButtonListener {
|
||||
virtual void update_button(bool state) = 0;
|
||||
};
|
||||
|
||||
class CST226Touchscreen final : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
class CST226Touchscreen final : public cstxxx::CSTTouchscreen {
|
||||
public:
|
||||
void setup() override;
|
||||
void update_touches() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
bool can_proceed() override { return this->setup_complete_ || this->is_failed(); }
|
||||
void register_button_listener(CST226ButtonListener *listener) { this->button_listeners_.push_back(listener); }
|
||||
|
||||
protected:
|
||||
bool read16_(uint16_t addr, uint8_t *data, size_t len);
|
||||
void continue_setup_();
|
||||
void update_button_state_(bool state);
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
uint8_t chip_id_{};
|
||||
bool setup_complete_{};
|
||||
std::vector<CST226ButtonListener *> button_listeners_;
|
||||
bool button_touched_{};
|
||||
uint16_t chip_id_{};
|
||||
uint16_t project_id_{};
|
||||
std::vector<CST226ButtonListener *> button_listeners_;
|
||||
};
|
||||
|
||||
} // namespace esphome::cst226
|
||||
@@ -1,37 +1,15 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
|
||||
from esphome.components import cstxxx
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import cst328_ns
|
||||
|
||||
CST328Touchscreen = cst328_ns.class_(
|
||||
"CST328Touchscreen",
|
||||
touchscreen.Touchscreen,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
AUTO_LOAD = ["cstxxx"]
|
||||
|
||||
CST328Touchscreen = cst328_ns.class_("CST328Touchscreen", cstxxx.CSTTouchscreen)
|
||||
|
||||
CST328ButtonListener = cst328_ns.class_("CST328ButtonListener")
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
touchscreen.touchscreen_schema("100ms")
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CST328Touchscreen),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x1A))
|
||||
)
|
||||
CONFIG_SCHEMA = cstxxx.cst_touchscreen_schema(CST328Touchscreen, 0x1A)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
|
||||
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
|
||||
await cstxxx.new_cst_touchscreen(config)
|
||||
@@ -5,31 +5,8 @@ namespace esphome::cst328 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cst328.touchscreen");
|
||||
|
||||
static const uint32_t CST328_BEFORE_RESET_TIMEOUT = 50; // 50 ms from datasheet
|
||||
static const uint32_t CST328_TRANSITION_TIMEOUT = 300; // 200 ms from datasheet, but typically much less
|
||||
static const uint16_t CST328_FW_CRC = 0xCACA; // Expected firmware CRC value
|
||||
static const uint8_t CST328_SYNC_BYTE = 0xAB; // Sync byte used in communication
|
||||
|
||||
static const uint8_t ZERO_BYTE = 0;
|
||||
|
||||
#define I2C_WARN_ON_ERROR(x, log_tag, format, ...) \
|
||||
do { \
|
||||
i2c::ErrorCode err_rc_ = (x); \
|
||||
if (err_rc_ != i2c::ERROR_OK) { \
|
||||
ESP_LOGW(log_tag, "%s(%d): [error %d] " format, __FUNCTION__, __LINE__, err_rc_, ##__VA_ARGS__); \
|
||||
this->status_set_warning(format); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define I2C_FAIL_ON_ERROR(x, log_tag, format, ...) \
|
||||
do { \
|
||||
i2c::ErrorCode err_rc_ = (x); \
|
||||
if (err_rc_ != i2c::ERROR_OK) { \
|
||||
ESP_LOGE(log_tag, "%s(%d): [error %d] " format, __FUNCTION__, __LINE__, err_rc_, ##__VA_ARGS__); \
|
||||
this->mark_failed(); \
|
||||
return; \
|
||||
} \
|
||||
} while (0)
|
||||
static constexpr uint32_t CST328_BEFORE_RESET_TIMEOUT = 50; // 50 ms from datasheet
|
||||
static constexpr uint32_t CST328_TRANSITION_TIMEOUT = 300; // 200 ms from datasheet, but typically much less
|
||||
|
||||
void CST328Touchscreen::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up CST328 Touchscreen...");
|
||||
@@ -50,50 +27,36 @@ void CST328Touchscreen::reset_device_() {
|
||||
void CST328Touchscreen::continue_setup_() {
|
||||
ESP_LOGV(TAG, "Continuing CST328 setup...");
|
||||
|
||||
uint8_t data_byte{0};
|
||||
uint8_t buf[24]{};
|
||||
|
||||
I2C_FAIL_ON_ERROR(this->write_register16(CST_WM_DEBUG_INFO, buf, 0), TAG, "Failed to enter debug/info mode");
|
||||
I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_FW_CRC_AND_BOOT_TIME, buf, 4), TAG,
|
||||
"Failed to read FW CRC and boot time");
|
||||
|
||||
uint16_t fw_crc = buf[2] + (buf[3] << 8);
|
||||
if (fw_crc != CST328_FW_CRC) {
|
||||
ESP_LOGE(TAG, "Error: Firmware CRC mismatch, expected 0x%04X but got 0x%04X", CST328_FW_CRC, fw_crc);
|
||||
this->mark_failed();
|
||||
if (!this->enter_command_mode_() || !this->check_firmware_())
|
||||
return;
|
||||
if (!this->setup_check(this->read_words_(cstxxx::REG_CHIP_INFO, this->project_id_, this->chip_id_),
|
||||
LOG_STR("Failed to read chip and project ID")))
|
||||
return;
|
||||
}
|
||||
|
||||
I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_CHIP_TYPE_AND_PROJECT_ID, buf, 4), TAG,
|
||||
"Failed to read chip and project ID");
|
||||
|
||||
this->chip_id_ = buf[2] + (buf[3] << 8);
|
||||
this->project_id_ = buf[0] + (buf[1] << 8);
|
||||
ESP_LOGD(TAG, "Chip ID %X, project ID %X", this->chip_id_, this->project_id_);
|
||||
I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_FW_REVISION, buf, 4), TAG, "Failed to read FW version");
|
||||
|
||||
this->fw_ver_major_ = buf[3];
|
||||
this->fw_ver_minor_ = buf[2];
|
||||
this->fw_build_ = buf[0] + (buf[1] << 8);
|
||||
uint16_t fw_version;
|
||||
if (!this->setup_check(this->read_words_(cstxxx::REG_FW_VERSION, this->fw_build_, fw_version),
|
||||
LOG_STR("Failed to read FW version")))
|
||||
return;
|
||||
this->fw_ver_major_ = fw_version >> 8;
|
||||
this->fw_ver_minor_ = fw_version & 0xFF;
|
||||
ESP_LOGV(TAG, "FW version %d.%d.%d", this->fw_ver_major_, this->fw_ver_minor_, this->fw_build_);
|
||||
|
||||
if (i2c::ERROR_OK == this->read_register16(CST_REG_X_Y_RESOLUTION, buf, 4)) {
|
||||
this->x_raw_max_ = buf[0] + (buf[1] << 8);
|
||||
this->y_raw_max_ = buf[2] + (buf[3] << 8);
|
||||
uint16_t x_max, y_max;
|
||||
if (this->read_words_(cstxxx::REG_RESOLUTION, x_max, y_max)) {
|
||||
this->x_raw_max_ = x_max;
|
||||
this->y_raw_max_ = y_max;
|
||||
} else {
|
||||
this->x_raw_max_ = this->display_->get_native_width();
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
|
||||
I2C_WARN_ON_ERROR(this->write_register16(CST_WM_NORMAL, buf, 0), TAG, "Failed to enter normal mode");
|
||||
I2C_WARN_ON_ERROR(this->read_register16(CST_REG_TOUCH_INFORMATION, &data_byte, 1), TAG, "Failed to read sync");
|
||||
I2C_WARN_ON_ERROR(this->write_register16(CST_REG_TOUCH_INFORMATION, &CST328_SYNC_BYTE, 1), TAG,
|
||||
"Failed to write sync");
|
||||
uint8_t sync;
|
||||
bool ok = this->write_register16(cstxxx::REG_NORMAL_MODE, nullptr, 0) == i2c::ERROR_OK;
|
||||
ok &= this->read_byte_reg16(cstxxx::REG_TOUCH_DATA, &sync);
|
||||
ok &= this->write_byte_reg16(cstxxx::REG_TOUCH_DATA, cstxxx::SYNC_BYTE);
|
||||
this->status_comm_check(ok);
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
this->setup_interrupt_pin_(gpio::INTERRUPT_FALLING_EDGE);
|
||||
|
||||
this->setup_complete_ = true;
|
||||
ESP_LOGV(TAG, "CST328 setup complete");
|
||||
@@ -142,21 +105,15 @@ void CST328Touchscreen::update_touches() {
|
||||
} else {
|
||||
this->update_button_state_(true);
|
||||
|
||||
uint8_t data_idx = 0;
|
||||
for (uint8_t i = 0; i < touch_cnt; i++) {
|
||||
uint8_t id = touch_data[data_idx] >> 4;
|
||||
int16_t x = (touch_data[data_idx + 1] << 4) | ((touch_data[data_idx + 3] >> 4) & 0x0F);
|
||||
int16_t y = (touch_data[data_idx + 2] << 4) | (touch_data[data_idx + 3] & 0x0F);
|
||||
int16_t z = touch_data[data_idx + 4];
|
||||
|
||||
this->add_raw_touch_position_(id, x, y, z);
|
||||
data_idx += (i == 0) ? 7 : 5;
|
||||
auto touch = cstxxx::decode_touch(touch_data, i);
|
||||
this->add_raw_touch_position_(touch.id, touch.x, touch.y, touch.z);
|
||||
}
|
||||
}
|
||||
|
||||
bool cleanup_error = false;
|
||||
cleanup_error |= !this->write_byte_reg16(CST_REG_TOUCH_FINGER_NUMBER, ZERO_BYTE);
|
||||
cleanup_error |= !this->write_byte_reg16(CST_REG_TOUCH_INFORMATION, CST328_SYNC_BYTE);
|
||||
cleanup_error |= !this->write_byte_reg16(cstxxx::REG_FINGER_COUNT, 0);
|
||||
cleanup_error |= !this->write_byte_reg16(cstxxx::REG_TOUCH_DATA, cstxxx::SYNC_BYTE);
|
||||
|
||||
if (cleanup_error) {
|
||||
ESP_LOGW(TAG, "Failed to clean up touch registers");
|
||||
|
||||
@@ -1,61 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/components/cstxxx/cstxxx.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::cst328 {
|
||||
|
||||
static const uint8_t CST328_TOUCH_MAX_POINTS = 5;
|
||||
static const uint8_t CST328_TOUCH_DATA_SIZE = CST328_TOUCH_MAX_POINTS * 5 + 2;
|
||||
static constexpr uint8_t CST328_TOUCH_MAX_POINTS = 5;
|
||||
static constexpr uint8_t CST328_TOUCH_DATA_SIZE = cstxxx::report_size(CST328_TOUCH_MAX_POINTS);
|
||||
|
||||
static const uint16_t CST_REG_TOUCH_INFORMATION = 0xD000;
|
||||
static const uint16_t CST_REG_TOUCH_FINGER_NUMBER = 0xD005;
|
||||
|
||||
static const uint16_t CST_REG_FINGER_COUNT_IDX = CST_REG_TOUCH_FINGER_NUMBER - CST_REG_TOUCH_INFORMATION;
|
||||
|
||||
static const uint16_t CST_REG_X_Y_RESOLUTION = 0xD1F8;
|
||||
static const uint16_t CST_REG_FW_CRC_AND_BOOT_TIME = 0xD1FC;
|
||||
static const uint16_t CST_REG_CHIP_TYPE_AND_PROJECT_ID = 0xD204;
|
||||
static const uint16_t CST_REG_FW_REVISION = 0xD208;
|
||||
|
||||
static const uint16_t CST_WM_DEBUG_INFO = 0xD101;
|
||||
static const uint16_t CST_WM_NORMAL = 0xD109;
|
||||
static constexpr uint16_t CST_REG_TOUCH_INFORMATION = cstxxx::REG_TOUCH_DATA;
|
||||
static constexpr uint16_t CST_REG_FINGER_COUNT_IDX = cstxxx::FINGER_COUNT_IDX;
|
||||
static constexpr uint16_t CST_REG_X_Y_RESOLUTION = cstxxx::REG_RESOLUTION;
|
||||
static constexpr uint16_t CST_REG_FW_CRC_AND_BOOT_TIME = cstxxx::REG_FW_CHECK_CODE;
|
||||
|
||||
class CST328ButtonListener {
|
||||
public:
|
||||
virtual void update_button(bool state) = 0;
|
||||
};
|
||||
|
||||
class CST328Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
class CST328Touchscreen : public cstxxx::CSTTouchscreen {
|
||||
public:
|
||||
void setup() override;
|
||||
void register_button_listener(CST328ButtonListener *listener) { this->button_listeners_.push_back(listener); }
|
||||
void dump_config() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
void update_touches() override;
|
||||
void reset_device_();
|
||||
void continue_setup_();
|
||||
void update_button_state_(bool state);
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
|
||||
std::vector<CST328ButtonListener *> button_listeners_;
|
||||
bool button_touched_{};
|
||||
|
||||
uint16_t chip_id_{};
|
||||
uint16_t project_id_{};
|
||||
uint8_t fw_ver_major_{};
|
||||
uint8_t fw_ver_minor_{};
|
||||
std::vector<CST328ButtonListener *> button_listeners_;
|
||||
uint16_t chip_id_{};
|
||||
uint16_t project_id_{};
|
||||
uint16_t fw_build_{};
|
||||
|
||||
bool setup_complete_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::cst328
|
||||
@@ -1,35 +1,14 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
|
||||
from esphome.components import cstxxx
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import cst9220_ns
|
||||
|
||||
CST9220Touchscreen = cst9220_ns.class_(
|
||||
"CST9220Touchscreen",
|
||||
touchscreen.Touchscreen,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
AUTO_LOAD = ["cstxxx"]
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
touchscreen.touchscreen_schema("100ms")
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CST9220Touchscreen),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x5A))
|
||||
)
|
||||
CST9220Touchscreen = cst9220_ns.class_("CST9220Touchscreen", cstxxx.CSTTouchscreen)
|
||||
|
||||
CONFIG_SCHEMA = cstxxx.cst_touchscreen_schema(CST9220Touchscreen, 0x5A)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
|
||||
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
|
||||
await cstxxx.new_cst_touchscreen(config)
|
||||
@@ -1,7 +1,4 @@
|
||||
#include "cst9220_touchscreen.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::cst9220 {
|
||||
|
||||
@@ -16,55 +13,29 @@ void CST9220Touchscreen::setup() {
|
||||
}
|
||||
|
||||
void CST9220Touchscreen::continue_setup_() {
|
||||
uint8_t buffer[4];
|
||||
this->setup_interrupt_pin_(gpio::INTERRUPT_FALLING_EDGE);
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
|
||||
// Enter command mode so the configuration registers can be read.
|
||||
if (this->write_register16(REG_CMD_MODE, buffer, 0) != i2c::ERROR_OK) {
|
||||
this->status_set_error(LOG_STR("Failed to enter command mode"));
|
||||
this->mark_failed();
|
||||
if (!this->enter_command_mode_())
|
||||
return;
|
||||
}
|
||||
delay(10);
|
||||
|
||||
// The firmware check code confirms that valid firmware is loaded.
|
||||
if (this->read_register16(REG_CHECKCODE, buffer, 4) != i2c::ERROR_OK) {
|
||||
this->status_set_error(LOG_STR("Failed to read check code"));
|
||||
this->mark_failed();
|
||||
if (!this->check_firmware_())
|
||||
return;
|
||||
}
|
||||
uint32_t checkcode = encode_uint32(buffer[3], buffer[2], buffer[1], buffer[0]);
|
||||
if ((checkcode & 0xFFFF0000) != 0xCACA0000) {
|
||||
ESP_LOGE(TAG, "Invalid firmware check code: 0x%08" PRIX32, checkcode);
|
||||
this->status_set_error(LOG_STR("Invalid firmware check code"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the panel resolution unless the user supplied calibration values.
|
||||
if (this->read_register16(REG_RESOLUTION, buffer, 4) == i2c::ERROR_OK) {
|
||||
uint16_t x_max, y_max;
|
||||
if (this->read_words_(cstxxx::REG_RESOLUTION, x_max, y_max)) {
|
||||
if (this->x_raw_max_ == this->x_raw_min_)
|
||||
this->x_raw_max_ = encode_value_le<uint16_t>(buffer);
|
||||
this->x_raw_max_ = x_max;
|
||||
if (this->y_raw_max_ == this->y_raw_min_)
|
||||
this->y_raw_max_ = encode_value_le<uint16_t>(&buffer[2]);
|
||||
this->y_raw_max_ = y_max;
|
||||
}
|
||||
|
||||
// Read the chip type and project id and validate the controller.
|
||||
if (this->read_register16(REG_CHIP_INFO, buffer, 4) != i2c::ERROR_OK) {
|
||||
this->status_set_error(LOG_STR("Failed to read chip ID"));
|
||||
this->mark_failed();
|
||||
if (!this->setup_check(this->read_words_(cstxxx::REG_CHIP_INFO, this->project_id_, this->chip_id_),
|
||||
LOG_STR("Failed to read chip ID")))
|
||||
return;
|
||||
}
|
||||
this->chip_id_ = encode_value_le<uint16_t>(&buffer[2]);
|
||||
this->project_id_ = encode_value_le<uint16_t>(buffer);
|
||||
if (this->chip_id_ != CST9220_CHIP_ID && this->chip_id_ != CST9217_CHIP_ID) {
|
||||
ESP_LOGE(TAG, "Unknown chip ID: 0x%04X", this->chip_id_);
|
||||
this->status_set_error(LOG_STR("Unknown chip ID"));
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Unknown chip ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,7 +55,7 @@ void CST9220Touchscreen::update_touches() {
|
||||
// Only an actual I2C failure should skip the update; a successful read with no
|
||||
// touches is a real "all fingers lifted" state that must flow through so the
|
||||
// base class can generate the release event.
|
||||
if (!this->status_comm_result(this->read_register16(REG_TOUCH_DATA, data, sizeof(data)) == i2c::ERROR_OK)) {
|
||||
if (!this->status_comm_result(this->read_register16(cstxxx::REG_TOUCH_DATA, data, sizeof(data)) == i2c::ERROR_OK)) {
|
||||
this->skip_update_ = true;
|
||||
return;
|
||||
}
|
||||
@@ -92,30 +63,23 @@ void CST9220Touchscreen::update_touches() {
|
||||
// A valid report carries the marker at offset 6. Writing an acknowledge back makes the controller return
|
||||
// invalid frames during a long press, so none is written. An invalid frame keeps the previous points; while
|
||||
// a finger is down, read again shortly so a lost release frame does not leave the touch held.
|
||||
if (data[6] != TOUCH_ACK) {
|
||||
if (data[cstxxx::SYNC_IDX] != cstxxx::SYNC_BYTE) {
|
||||
this->skip_update_ = true;
|
||||
if (!this->touches_.empty())
|
||||
this->set_timeout(REREAD_TIMEOUT_ID, REREAD_DELAY_MS, [this] { this->store_.touched = true; });
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t num_touches = data[5] & 0x7F;
|
||||
uint8_t num_touches = data[cstxxx::FINGER_COUNT_IDX] & 0x7F;
|
||||
if (num_touches > CST9220_MAX_TOUCHES)
|
||||
num_touches = CST9220_MAX_TOUCHES;
|
||||
|
||||
for (uint8_t i = 0; i < num_touches; i++) {
|
||||
// The first point starts at offset 0; subsequent points are offset by the
|
||||
// two status bytes that follow it.
|
||||
const uint8_t *p = data + i * 5 + (i == 0 ? 0 : 2);
|
||||
uint8_t id = p[0] >> 4;
|
||||
uint8_t event = p[0] & 0x0F;
|
||||
if (event != TOUCH_EVENT_DOWN)
|
||||
auto touch = cstxxx::decode_touch(data, i);
|
||||
if (touch.event != TOUCH_EVENT_DOWN)
|
||||
continue;
|
||||
// p[3] is shared: high nibble holds the X LSBs, low nibble the Y LSBs.
|
||||
uint16_t x = (p[1] << 4) | (p[3] >> 4);
|
||||
uint16_t y = (p[2] << 4) | (p[3] & 0x0F);
|
||||
ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
|
||||
this->add_raw_touch_position_(id, x, y);
|
||||
ESP_LOGV(TAG, "Read touch %d: %d/%d", touch.id, touch.x, touch.y);
|
||||
this->add_raw_touch_position_(touch.id, touch.x, touch.y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/components/cstxxx/cstxxx.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -10,41 +9,26 @@ namespace esphome::cst9220 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cst9220.touchscreen");
|
||||
|
||||
// The CST92xx family uses 16-bit (big-endian) register addresses.
|
||||
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
|
||||
static const uint16_t REG_CMD_MODE = 0xD101; // enter command mode
|
||||
static const uint16_t REG_CHECKCODE = 0xD1FC; // firmware check code
|
||||
static const uint16_t REG_RESOLUTION = 0xD1F8; // panel resolution
|
||||
static const uint16_t REG_CHIP_INFO = 0xD204; // chip type + project id
|
||||
static constexpr uint8_t TOUCH_EVENT_DOWN = 0x06;
|
||||
|
||||
static const uint8_t TOUCH_ACK = 0xAB;
|
||||
static const uint8_t TOUCH_EVENT_DOWN = 0x06;
|
||||
static constexpr uint16_t CST9220_CHIP_ID = 0x9220;
|
||||
static constexpr uint16_t CST9217_CHIP_ID = 0x9217;
|
||||
|
||||
static const uint16_t CST9220_CHIP_ID = 0x9220;
|
||||
static const uint16_t CST9217_CHIP_ID = 0x9217;
|
||||
|
||||
// Maximum simultaneous touch points reported by the family.
|
||||
static const uint8_t CST9220_MAX_TOUCHES = 5;
|
||||
static constexpr uint8_t CST9220_MAX_TOUCHES = 5;
|
||||
// Report layout: 5 bytes per touch point plus 5 bytes of status/ack overhead.
|
||||
static const size_t CST9220_DATA_LENGTH = CST9220_MAX_TOUCHES * 5 + 5;
|
||||
static constexpr size_t CST9220_DATA_LENGTH = CST9220_MAX_TOUCHES * 5 + 5;
|
||||
|
||||
class CST9220Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
class CST9220Touchscreen : public cstxxx::CSTTouchscreen {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
void update_touches() override;
|
||||
void continue_setup_();
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
uint16_t chip_id_{};
|
||||
uint16_t project_id_{};
|
||||
bool setup_complete_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::cst9220
|
||||
@@ -0,0 +1,42 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@clydebarrow", "@latonita"]
|
||||
DEPENDENCIES = ["i2c", "touchscreen"]
|
||||
DOMAIN = "cstxxx"
|
||||
|
||||
cstxxx_ns = cg.esphome_ns.namespace("cstxxx")
|
||||
CSTTouchscreen = cstxxx_ns.class_(
|
||||
"CSTTouchscreen", touchscreen.Touchscreen, i2c.I2CDevice
|
||||
)
|
||||
|
||||
|
||||
def cst_touchscreen_schema(touchscreen_class: MockObjClass, address: int) -> cv.Schema:
|
||||
"""Return the touchscreen schema of a Hynitron driver."""
|
||||
return (
|
||||
touchscreen.touchscreen_schema("100ms")
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(touchscreen_class),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(address))
|
||||
)
|
||||
|
||||
|
||||
async def new_cst_touchscreen(config: ConfigType) -> MockObj:
|
||||
"""Create a Hynitron touchscreen and set its pins."""
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
|
||||
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
|
||||
return var
|
||||
@@ -0,0 +1,13 @@
|
||||
#include "cstxxx.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::cstxxx {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cstxxx");
|
||||
|
||||
void CSTTouchscreen::fail_check_code_(uint16_t check_code) {
|
||||
ESP_LOGE(TAG, "Firmware check code 0x%04X", check_code);
|
||||
this->mark_failed(LOG_STR("Invalid firmware check code"));
|
||||
}
|
||||
|
||||
} // namespace esphome::cstxxx
|
||||
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::cstxxx {
|
||||
|
||||
static constexpr uint16_t REG_TOUCH_DATA = 0xD000;
|
||||
static constexpr uint16_t REG_FINGER_COUNT = 0xD005;
|
||||
static constexpr uint16_t REG_COMMAND_MODE = 0xD101;
|
||||
static constexpr uint16_t REG_NORMAL_MODE = 0xD109;
|
||||
static constexpr uint16_t REG_RESOLUTION = 0xD1F8;
|
||||
static constexpr uint16_t REG_FW_CHECK_CODE = 0xD1FC;
|
||||
static constexpr uint16_t REG_CHIP_INFO = 0xD204;
|
||||
static constexpr uint16_t REG_FW_VERSION = 0xD208;
|
||||
|
||||
static constexpr uint8_t FINGER_COUNT_IDX = REG_FINGER_COUNT - REG_TOUCH_DATA;
|
||||
static constexpr uint8_t SYNC_IDX = FINGER_COUNT_IDX + 1;
|
||||
static constexpr uint8_t SYNC_BYTE = 0xAB;
|
||||
static constexpr uint16_t FW_CHECK_CODE = 0xCACA;
|
||||
|
||||
/// Bytes of a touch report holding `points` records; two status bytes follow the first record.
|
||||
constexpr size_t report_size(uint8_t points) { return points * 5 + 2; }
|
||||
|
||||
struct TouchRecord {
|
||||
uint8_t id;
|
||||
uint8_t event;
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
uint8_t z;
|
||||
};
|
||||
|
||||
/// Decodes record `index` of a touch report.
|
||||
inline TouchRecord decode_touch(const uint8_t *report, uint8_t index) {
|
||||
const uint8_t *r = report + index * 5 + (index == 0 ? 0 : 2);
|
||||
return {static_cast<uint8_t>(r[0] >> 4), static_cast<uint8_t>(r[0] & 0x0F),
|
||||
static_cast<uint16_t>(r[1] << 4 | r[3] >> 4), static_cast<uint16_t>(r[2] << 4 | (r[3] & 0x0F)), r[4]};
|
||||
}
|
||||
|
||||
class CSTTouchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
public:
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
/// Sets up the interrupt pin, if configured, to fire on `type`.
|
||||
void setup_interrupt_pin_(gpio::InterruptType type) {
|
||||
if (this->interrupt_pin_ == nullptr)
|
||||
return;
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_, type);
|
||||
}
|
||||
/// Reads two little-endian words starting at `reg`.
|
||||
bool ESPHOME_ALWAYS_INLINE read_words_(uint16_t reg, uint16_t &low, uint16_t &high) {
|
||||
uint16_t words[2];
|
||||
if (this->read_register16(reg, reinterpret_cast<uint8_t *>(words), sizeof(words)) != i2c::ERROR_OK)
|
||||
return false;
|
||||
low = convert_little_endian(words[0]);
|
||||
high = convert_little_endian(words[1]);
|
||||
return true;
|
||||
}
|
||||
/// Enters command mode, marking the component failed if the write fails.
|
||||
bool enter_command_mode_() {
|
||||
return this->setup_check(this->write_register16(REG_COMMAND_MODE, nullptr, 0) == i2c::ERROR_OK,
|
||||
LOG_STR("Failed to enter command mode"));
|
||||
}
|
||||
/// Checks the firmware check code, marking the component failed if it is unreadable or wrong.
|
||||
bool check_firmware_() {
|
||||
uint16_t boot_time, check_code;
|
||||
if (!this->setup_check(this->read_words_(REG_FW_CHECK_CODE, boot_time, check_code)))
|
||||
return false;
|
||||
if (check_code == FW_CHECK_CODE)
|
||||
return true;
|
||||
this->fail_check_code_(check_code);
|
||||
return false;
|
||||
}
|
||||
/// Logs a wrong firmware check code and marks the component failed.
|
||||
void fail_check_code_(uint16_t check_code);
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
bool setup_complete_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::cstxxx
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#ifdef USE_ETHERNET
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -366,6 +368,9 @@ class EthernetComponent final : public Component {
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
StaticVector<EthernetIPStateListener *, ESPHOME_ETHERNET_IP_STATE_LISTENERS> ip_state_listeners_;
|
||||
#ifdef USE_ESP32
|
||||
std::atomic<uint8_t> ip_state_pending_{0};
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_ETHERNET_CONNECT_TRIGGER
|
||||
|
||||
@@ -129,6 +129,14 @@ void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *me
|
||||
void EthernetComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
// The IP events arrive on the event task; the listeners publish states, so notify them here
|
||||
if (this->ip_state_pending_.load(std::memory_order_acquire) != 0) {
|
||||
this->ip_state_pending_.store(0, std::memory_order_relaxed);
|
||||
this->notify_ip_state_listeners_();
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (this->state_) {
|
||||
case EthernetComponentState::STOPPED:
|
||||
if (this->started_) {
|
||||
@@ -770,7 +778,8 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
|
||||
event_name = "ETH connected";
|
||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
|
||||
global_eth_component->notify_ip_state_listeners_();
|
||||
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
|
||||
global_eth_component->enable_loop_soon_any_context();
|
||||
#endif
|
||||
#if USE_NETWORK_IPV6
|
||||
// Start SLAAC on link-up, not after the DHCPv4 lease. This also restores the
|
||||
@@ -798,6 +807,9 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
|
||||
const esp_netif_ip_info_t *ip_info = &event->ip_info;
|
||||
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip));
|
||||
global_eth_component->got_ipv4_address_ = true;
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
|
||||
#endif
|
||||
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
|
||||
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
@@ -805,9 +817,6 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
|
||||
global_eth_component->connected_ = true;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
global_eth_component->notify_ip_state_listeners_();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
@@ -819,6 +828,9 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
|
||||
// after a link flap fires another event for the same address.
|
||||
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||
global_eth_component->ipv6_count_ = esp_netif_get_all_ip6(global_eth_component->eth_netif_, if_ip6s);
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
|
||||
#endif
|
||||
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
|
||||
global_eth_component->connected_ =
|
||||
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
|
||||
@@ -827,9 +839,6 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
|
||||
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
global_eth_component->notify_ip_state_listeners_();
|
||||
#endif
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ CONF_WHITENING_ENABLE = "whitening_enable"
|
||||
CONF_WHITENING_INITIAL = "whitening_initial"
|
||||
|
||||
sx126x_ns = cg.esphome_ns.namespace("sx126x")
|
||||
SX126x = sx126x_ns.class_("SX126x", cg.Component, spi.SPIDevice)
|
||||
SX126x = sx126x_ns.class_("SX126x", cg.Component)
|
||||
SX126xListener = sx126x_ns.class_("SX126xListener")
|
||||
SX126xBw = sx126x_ns.enum("SX126xBw")
|
||||
SX126xPacketType = sx126x_ns.enum("SX126xPacketType")
|
||||
@@ -217,7 +217,9 @@ CONFIG_SCHEMA = (
|
||||
cv.Required(CONF_RF_SWITCH): cv.boolean,
|
||||
cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING),
|
||||
cv.Optional(CONF_SPREADING_FACTOR, default=7): cv.int_range(min=6, max=12),
|
||||
cv.Optional(CONF_SYNC_VALUE, default=[]): cv.ensure_list(cv.hex_uint8_t),
|
||||
cv.Optional(CONF_SYNC_VALUE, default=[]): cv.All(
|
||||
cv.ensure_list(cv.hex_uint8_t), cv.Length(max=8)
|
||||
),
|
||||
cv.Optional(CONF_TCXO_VOLTAGE, default="NONE"): cv.enum(TCXO_VOLTAGE),
|
||||
cv.Optional(CONF_TCXO_DELAY, default="5ms"): cv.All(
|
||||
cv.positive_time_period_microseconds,
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
#include "sx126x_reg.h"
|
||||
#include <array>
|
||||
#include <span>
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
enum SX126xBw : uint8_t {
|
||||
// FSK
|
||||
SX126X_BW_4800,
|
||||
SX126X_BW_5800,
|
||||
SX126X_BW_7300,
|
||||
SX126X_BW_9700,
|
||||
SX126X_BW_11700,
|
||||
SX126X_BW_14600,
|
||||
SX126X_BW_19500,
|
||||
SX126X_BW_23400,
|
||||
SX126X_BW_29300,
|
||||
SX126X_BW_39000,
|
||||
SX126X_BW_46900,
|
||||
SX126X_BW_58600,
|
||||
SX126X_BW_78200,
|
||||
SX126X_BW_93800,
|
||||
SX126X_BW_117300,
|
||||
SX126X_BW_156200,
|
||||
SX126X_BW_187200,
|
||||
SX126X_BW_234300,
|
||||
SX126X_BW_312000,
|
||||
SX126X_BW_373600,
|
||||
SX126X_BW_467000,
|
||||
// LORA
|
||||
SX126X_BW_7810,
|
||||
SX126X_BW_10420,
|
||||
SX126X_BW_15630,
|
||||
SX126X_BW_20830,
|
||||
SX126X_BW_31250,
|
||||
SX126X_BW_41670,
|
||||
SX126X_BW_62500,
|
||||
SX126X_BW_125000,
|
||||
SX126X_BW_250000,
|
||||
SX126X_BW_500000,
|
||||
};
|
||||
|
||||
// Requested settings are separate from the last successfully applied profile.
|
||||
// Bounded sync storage makes profile snapshots allocation-free.
|
||||
struct RadioProfile {
|
||||
uint32_t bitrate{0};
|
||||
uint32_t deviation{0};
|
||||
uint32_t frequency{0};
|
||||
uint32_t tcxo_delay{0};
|
||||
uint16_t crc_polynomial{0};
|
||||
uint16_t crc_initial{0};
|
||||
uint16_t whitening_initial{0};
|
||||
uint16_t preamble_detect{0};
|
||||
uint16_t preamble_size{0};
|
||||
SX126xBw bandwidth{SX126X_BW_125000};
|
||||
uint8_t crc_size{0};
|
||||
uint8_t tcxo_voltage{0};
|
||||
uint8_t coding_rate{0};
|
||||
uint8_t modulation{PACKET_TYPE_LORA};
|
||||
uint8_t pa_ramp{0};
|
||||
uint8_t shaping{0};
|
||||
uint8_t spreading_factor{0};
|
||||
int8_t pa_power{0};
|
||||
uint8_t payload_length{0};
|
||||
bool sx1261{false};
|
||||
bool crc_enable{false};
|
||||
bool crc_inverted{false};
|
||||
bool whitening_enable{false};
|
||||
bool rx_start{false};
|
||||
bool rf_switch{false};
|
||||
std::array<uint8_t, 8> sync_value{};
|
||||
uint8_t sync_size{0};
|
||||
bool sync_valid{true};
|
||||
void set_sync_value(std::span<const uint8_t> bytes) {
|
||||
this->sync_valid = bytes.size() <= this->sync_value.size();
|
||||
if (!this->sync_valid) {
|
||||
return;
|
||||
}
|
||||
std::copy(bytes.begin(), bytes.end(), this->sync_value.begin());
|
||||
this->sync_size = bytes.size();
|
||||
}
|
||||
size_t max_packet_size() const { return this->payload_length > 0 ? this->payload_length : 255; }
|
||||
};
|
||||
|
||||
// The parts of the last successfully applied profile that runtime operations read.
|
||||
// SetPacketParams is encoded once; sends only change its payload length byte.
|
||||
struct AppliedProfile {
|
||||
uint32_t frequency{0};
|
||||
std::array<uint8_t, 9> packet_params{};
|
||||
uint8_t packet_params_size{0};
|
||||
uint8_t payload_length{0};
|
||||
uint8_t modulation{PACKET_TYPE_LORA};
|
||||
bool rx_start{false};
|
||||
size_t max_packet_size() const { return this->payload_length > 0 ? this->payload_length : 255; }
|
||||
};
|
||||
} // namespace esphome::sx126x
|
||||
@@ -2,534 +2,217 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "sx126x");
|
||||
static constexpr uint16_t RAMP[8] PROGMEM = {10, 20, 40, 80, 200, 800, 1700, 3400};
|
||||
// Aligned 32 bit loads from flash are safe on ESP8266, so this table is read directly
|
||||
static constexpr uint32_t BW_HZ[31] PROGMEM = {4800, 5800, 7300, 9700, 11700, 14600, 19500, 23400,
|
||||
29300, 39000, 46900, 58600, 78200, 93800, 117300, 156200,
|
||||
187200, 234300, 312000, 373600, 467000, 7810, 10420, 15630,
|
||||
20830, 31250, 41670, 62500, 125000, 250000, 500000};
|
||||
static constexpr uint8_t BW_LORA[10] PROGMEM = {LORA_BW_7810, LORA_BW_10420, LORA_BW_15630, LORA_BW_20830,
|
||||
LORA_BW_31250, LORA_BW_41670, LORA_BW_62500, LORA_BW_125000,
|
||||
LORA_BW_250000, LORA_BW_500000};
|
||||
static constexpr uint8_t BW_FSK[21] PROGMEM = {
|
||||
FSK_BW_4800, FSK_BW_5800, FSK_BW_7300, FSK_BW_9700, FSK_BW_11700, FSK_BW_14600, FSK_BW_19500,
|
||||
FSK_BW_23400, FSK_BW_29300, FSK_BW_39000, FSK_BW_46900, FSK_BW_58600, FSK_BW_78200, FSK_BW_93800,
|
||||
FSK_BW_117300, FSK_BW_156200, FSK_BW_187200, FSK_BW_234300, FSK_BW_312000, FSK_BW_373600, FSK_BW_467000};
|
||||
|
||||
static constexpr uint32_t RESET_DELAY_HIGH_US = 5000;
|
||||
static constexpr uint32_t RESET_DELAY_LOW_US = 2000;
|
||||
static constexpr uint32_t SWITCHING_DELAY_US = 1;
|
||||
static constexpr uint32_t TRANSMIT_TIMEOUT_MS = 4000;
|
||||
static constexpr uint32_t BUSY_TIMEOUT_MS = 20;
|
||||
static constexpr uint32_t MAX_TRANSMIT_TIMEOUT_MS = 262143; // 24-bit SetTx timeout in 1/64 ms steps
|
||||
static constexpr uint32_t RECONFIGURE_INTERVAL_ID = 0;
|
||||
static constexpr uint32_t RECONFIGURE_INTERVAL_MS = 1000;
|
||||
|
||||
// OCP (Over Current Protection) values
|
||||
static constexpr uint8_t OCP_80MA = 0x18; // 80 mA max current
|
||||
static constexpr uint8_t OCP_140MA = 0x38; // 140 mA max current
|
||||
|
||||
// LoRa low data rate optimization threshold
|
||||
static constexpr float LOW_DATA_RATE_OPTIMIZE_THRESHOLD = 16.38f; // 16.38 ms
|
||||
|
||||
uint8_t SX126x::read_fifo_(uint8_t offset, std::vector<uint8_t> &packet) {
|
||||
this->enable();
|
||||
this->wait_busy_();
|
||||
this->transfer_byte(RADIO_READ_BUFFER);
|
||||
this->transfer_byte(offset);
|
||||
uint8_t status = this->transfer_byte(0x00);
|
||||
for (uint8_t &byte : packet) {
|
||||
byte = this->transfer_byte(0x00);
|
||||
}
|
||||
this->disable();
|
||||
return status;
|
||||
}
|
||||
|
||||
void SX126x::write_fifo_(uint8_t offset, const uint8_t *data, size_t len) {
|
||||
this->enable();
|
||||
this->wait_busy_();
|
||||
this->transfer_byte(RADIO_WRITE_BUFFER);
|
||||
this->transfer_byte(offset);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->transfer_byte(data[i]);
|
||||
}
|
||||
this->disable();
|
||||
delayMicroseconds(SWITCHING_DELAY_US);
|
||||
}
|
||||
|
||||
uint8_t SX126x::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
|
||||
this->enable();
|
||||
this->wait_busy_();
|
||||
this->transfer_byte(opcode);
|
||||
uint8_t status = this->transfer_byte(0x00);
|
||||
for (int32_t i = 0; i < size; i++) {
|
||||
data[i] = this->transfer_byte(0x00);
|
||||
}
|
||||
this->disable();
|
||||
return status;
|
||||
}
|
||||
|
||||
void SX126x::write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
|
||||
this->enable();
|
||||
this->wait_busy_();
|
||||
this->transfer_byte(opcode);
|
||||
for (int32_t i = 0; i < size; i++) {
|
||||
this->transfer_byte(data[i]);
|
||||
}
|
||||
this->disable();
|
||||
delayMicroseconds(SWITCHING_DELAY_US);
|
||||
}
|
||||
|
||||
void SX126x::read_register_(uint16_t reg, uint8_t *data, uint8_t size) {
|
||||
this->enable();
|
||||
this->wait_busy_();
|
||||
this->write_byte(RADIO_READ_REGISTER);
|
||||
this->write_byte((reg >> 8) & 0xFF);
|
||||
this->write_byte((reg >> 0) & 0xFF);
|
||||
this->write_byte(0x00);
|
||||
for (int32_t i = 0; i < size; i++) {
|
||||
data[i] = this->transfer_byte(0x00);
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) {
|
||||
this->enable();
|
||||
this->wait_busy_();
|
||||
this->write_byte(RADIO_WRITE_REGISTER);
|
||||
this->write_byte((reg >> 8) & 0xFF);
|
||||
this->write_byte((reg >> 0) & 0xFF);
|
||||
for (int32_t i = 0; i < size; i++) {
|
||||
this->transfer_byte(data[i]);
|
||||
}
|
||||
this->disable();
|
||||
delayMicroseconds(SWITCHING_DELAY_US);
|
||||
}
|
||||
|
||||
void IRAM_ATTR SX126x::gpio_intr(SX126x *arg) { arg->enable_loop_soon_any_context(); }
|
||||
// Indexed by SX126xError.
|
||||
PROGMEM_STRING_TABLE(SX126xErrorStrings, "none", "timeout", "invalid payload", "radio not ready", "bus timeout",
|
||||
"radio busy", "cancelled");
|
||||
|
||||
void SX126x::setup() {
|
||||
// Sized once so later packet reads never reallocate; read_packet() sets the length.
|
||||
this->packet_.resize(SX126X_MAX_PACKET_SIZE);
|
||||
// setup pins
|
||||
this->busy_pin_->setup();
|
||||
this->rst_pin_->setup();
|
||||
this->dio1_pin_->setup();
|
||||
if (this->dio1_pin_->is_internal()) {
|
||||
static_cast<InternalGPIOPin *>(this->dio1_pin_)
|
||||
->attach_interrupt(&SX126x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
GPIOPin *dio1 = this->radio_.get_dio1_pin();
|
||||
this->radio_.bus().get_busy_pin()->setup();
|
||||
this->radio_.get_rst_pin()->setup();
|
||||
dio1->setup();
|
||||
if (dio1->is_internal()) {
|
||||
static_cast<InternalGPIOPin *>(dio1)->attach_interrupt(&SX126x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
|
||||
// start spi
|
||||
this->spi_setup();
|
||||
this->radio_.bus().setup();
|
||||
|
||||
// configure rf
|
||||
this->configure();
|
||||
// configure rf; a failure is reported through mark_failed() only, with its own reason
|
||||
const RadioEffects effects = this->radio_.configure(this->requested_profile_, this->version_);
|
||||
if (effects.warning != nullptr) {
|
||||
this->mark_failed(effects.warning);
|
||||
return;
|
||||
}
|
||||
this->apply_(effects);
|
||||
}
|
||||
|
||||
void SX126x::apply_(const RadioEffects &effects) {
|
||||
// User code may only run once the radio has committed. A rejection during another
|
||||
// operation's command sequence carries no status or callbacks.
|
||||
ESPHOME_DEBUG_ASSERT(!this->radio_.busy() || (!effects.report && effects.listener == nullptr && !effects.received));
|
||||
if (effects.sleep_loop) {
|
||||
this->disable_loop();
|
||||
} else if (this->radio_.state() != RadioState::RADIO_STATE_NEEDS_CONFIGURATION) {
|
||||
this->enable_loop();
|
||||
}
|
||||
this->watch_configuration_();
|
||||
// The packet buffer is borrowed from the status report until listeners return;
|
||||
// loop() will not refill it meanwhile.
|
||||
if (effects.received) {
|
||||
this->rx_dispatching_ = true;
|
||||
}
|
||||
if (effects.report) {
|
||||
this->status_comm_result(effects.warning == nullptr, effects.warning);
|
||||
}
|
||||
if (effects.listener != nullptr) {
|
||||
effects.listener->on_transmit_complete(effects.result);
|
||||
}
|
||||
if (effects.received) {
|
||||
for (auto *listener : this->listeners_) {
|
||||
listener->on_packet(this->packet_, effects.rssi, effects.snr);
|
||||
}
|
||||
this->packet_trigger_.trigger(this->packet_, effects.rssi, effects.snr);
|
||||
this->rx_dispatching_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SX126x::configure() {
|
||||
uint8_t buf[8];
|
||||
|
||||
// toggle chip reset
|
||||
this->rst_pin_->digital_write(true);
|
||||
delayMicroseconds(RESET_DELAY_HIGH_US);
|
||||
this->rst_pin_->digital_write(false);
|
||||
delayMicroseconds(RESET_DELAY_LOW_US);
|
||||
this->rst_pin_->digital_write(true);
|
||||
delayMicroseconds(RESET_DELAY_HIGH_US);
|
||||
|
||||
// wakeup
|
||||
this->read_opcode_(RADIO_GET_STATUS, nullptr, 0);
|
||||
|
||||
// config tcxo
|
||||
if (this->tcxo_voltage_ != TCXO_CTRL_NONE) {
|
||||
uint32_t delay = this->tcxo_delay_ >> 6;
|
||||
buf[0] = this->tcxo_voltage_;
|
||||
buf[1] = (delay >> 16) & 0xFF;
|
||||
buf[2] = (delay >> 8) & 0xFF;
|
||||
buf[3] = (delay >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_TCXOMODE, buf, 4);
|
||||
buf[0] = 0x7F;
|
||||
this->write_opcode_(RADIO_CALIBRATE, buf, 1);
|
||||
}
|
||||
|
||||
// clear errors
|
||||
buf[0] = 0x00;
|
||||
buf[1] = 0x00;
|
||||
this->write_opcode_(RADIO_CLR_ERROR, buf, 2);
|
||||
|
||||
// rf switch
|
||||
if (this->rf_switch_) {
|
||||
buf[0] = 0x01;
|
||||
this->write_opcode_(RADIO_SET_RFSWITCHMODE, buf, 1);
|
||||
}
|
||||
|
||||
// check silicon version to make sure hw is ok
|
||||
this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, sizeof(this->version_));
|
||||
this->version_[sizeof(this->version_) - 1] = '\0';
|
||||
if (strncmp(this->version_, "SX126", 5) != 0 && strncmp(this->version_, "LLCC68", 6) != 0) {
|
||||
this->mark_failed(LOG_STR("Unsupported chip version"));
|
||||
// Before setup() the bus is not set up, and after a failed setup the radio stays unused
|
||||
if (!this->is_ready()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// setup packet type
|
||||
buf[0] = this->modulation_;
|
||||
this->write_opcode_(RADIO_SET_PACKETTYPE, buf, 1);
|
||||
|
||||
// calibrate image
|
||||
this->run_image_cal();
|
||||
|
||||
// set frequency
|
||||
uint64_t freq = ((uint64_t) this->frequency_ << 25) / XTAL_FREQ;
|
||||
buf[0] = (uint8_t) ((freq >> 24) & 0xFF);
|
||||
buf[1] = (uint8_t) ((freq >> 16) & 0xFF);
|
||||
buf[2] = (uint8_t) ((freq >> 8) & 0xFF);
|
||||
buf[3] = (uint8_t) (freq & 0xFF);
|
||||
this->write_opcode_(RADIO_SET_RFFREQUENCY, buf, 4);
|
||||
|
||||
// configure pa
|
||||
int8_t pa_power = this->pa_power_;
|
||||
if (this->sx1261_) {
|
||||
// the following values were taken from section 13.1.14.1 table 13-21
|
||||
// in rev 2.1 of the datasheet
|
||||
if (pa_power == 15) {
|
||||
uint8_t cfg[4] = {0x06, 0x00, 0x01, 0x01};
|
||||
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
|
||||
} else {
|
||||
uint8_t cfg[4] = {0x04, 0x00, 0x01, 0x01};
|
||||
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
|
||||
}
|
||||
pa_power = std::max(pa_power, (int8_t) -3);
|
||||
pa_power = std::min(pa_power, (int8_t) 14);
|
||||
buf[0] = OCP_80MA;
|
||||
this->write_register_(REG_OCP, buf, 1);
|
||||
} else {
|
||||
// the following values were taken from section 13.1.14.1 table 13-21
|
||||
// in rev 2.1 of the datasheet
|
||||
uint8_t cfg[4] = {0x04, 0x07, 0x00, 0x01};
|
||||
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
|
||||
pa_power = std::max(pa_power, (int8_t) -3);
|
||||
pa_power = std::min(pa_power, (int8_t) 22);
|
||||
buf[0] = OCP_140MA;
|
||||
this->write_register_(REG_OCP, buf, 1);
|
||||
if (!this->radio_.can_configure()) {
|
||||
ESP_LOGW(TAG, "Radio operation in progress, configuration ignored");
|
||||
return;
|
||||
}
|
||||
buf[0] = pa_power;
|
||||
buf[1] = this->pa_ramp_;
|
||||
this->write_opcode_(RADIO_SET_TXPARAMS, buf, 2);
|
||||
this->cold_sleep_ = false;
|
||||
this->apply_(this->radio_.configure(this->requested_profile_, this->version_));
|
||||
}
|
||||
|
||||
// configure modem
|
||||
if (this->modulation_ == PACKET_TYPE_LORA) {
|
||||
// set modulation params
|
||||
float duration = 1000.0f * (1UL << this->spreading_factor_) / BW_HZ[this->bandwidth_];
|
||||
buf[0] = this->spreading_factor_;
|
||||
buf[1] = progmem_read_byte(&BW_LORA[this->bandwidth_ - SX126X_BW_7810]);
|
||||
buf[2] = this->coding_rate_;
|
||||
buf[3] = (duration > LOW_DATA_RATE_OPTIMIZE_THRESHOLD) ? 0x01 : 0x00;
|
||||
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4);
|
||||
|
||||
// set packet params and sync word
|
||||
this->set_packet_params_(this->get_max_packet_size());
|
||||
if (this->sync_value_.size() == 2) {
|
||||
this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
|
||||
}
|
||||
} else {
|
||||
// set modulation params
|
||||
uint32_t bitrate = ((uint64_t) XTAL_FREQ * 32) / this->bitrate_;
|
||||
uint32_t fdev = ((uint64_t) this->deviation_ << 25) / XTAL_FREQ;
|
||||
buf[0] = (bitrate >> 16) & 0xFF;
|
||||
buf[1] = (bitrate >> 8) & 0xFF;
|
||||
buf[2] = (bitrate >> 0) & 0xFF;
|
||||
buf[3] = this->shaping_;
|
||||
buf[4] = progmem_read_byte(&BW_FSK[this->bandwidth_ - SX126X_BW_4800]);
|
||||
buf[5] = (fdev >> 16) & 0xFF;
|
||||
buf[6] = (fdev >> 8) & 0xFF;
|
||||
buf[7] = (fdev >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8);
|
||||
|
||||
// set crc params
|
||||
if (this->crc_enable_) {
|
||||
buf[0] = this->crc_initial_ >> 8;
|
||||
buf[1] = this->crc_initial_ & 0xFF;
|
||||
this->write_register_(REG_CRC_INITIAL, buf, 2);
|
||||
buf[0] = this->crc_polynomial_ >> 8;
|
||||
buf[1] = this->crc_polynomial_ & 0xFF;
|
||||
this->write_register_(REG_CRC_POLYNOMIAL, buf, 2);
|
||||
}
|
||||
|
||||
// set whitening params
|
||||
if (this->whitening_enable_) {
|
||||
// according to the datasheet, section 12 table 12-1 "The user should not
|
||||
// change the value of the 7 MSB of this register"
|
||||
this->read_register_(REG_WHITENING_INITIAL, buf, 1);
|
||||
buf[0] = (buf[0] & 0xFE) | ((this->whitening_initial_ >> 8) & 0x01);
|
||||
buf[1] = this->whitening_initial_ & 0xFF;
|
||||
this->write_register_(REG_WHITENING_INITIAL, buf, 2);
|
||||
}
|
||||
|
||||
// set packet params and sync word
|
||||
this->set_packet_params_(this->get_max_packet_size());
|
||||
if (!this->sync_value_.empty()) {
|
||||
this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
|
||||
}
|
||||
// A failed command leaves the radio needing configuration, which nothing else would do, so a node
|
||||
// that only uses YAML would stay deaf. Cold sleep is asked for, so it waits for a mode command.
|
||||
void SX126x::watch_configuration_() {
|
||||
const bool needed = this->radio_.state() == RadioState::RADIO_STATE_NEEDS_CONFIGURATION && !this->cold_sleep_;
|
||||
if (needed == this->reconfigure_pending_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// switch to rx or sleep
|
||||
if (this->rx_start_) {
|
||||
this->set_mode_rx();
|
||||
this->reconfigure_pending_ = needed;
|
||||
if (needed) {
|
||||
this->set_interval(RECONFIGURE_INTERVAL_ID, RECONFIGURE_INTERVAL_MS, [this]() { this->configure(); });
|
||||
} else {
|
||||
this->set_mode_sleep();
|
||||
this->cancel_interval(RECONFIGURE_INTERVAL_ID);
|
||||
}
|
||||
}
|
||||
|
||||
size_t SX126x::get_max_packet_size() {
|
||||
if (this->payload_length_ > 0) {
|
||||
return this->payload_length_;
|
||||
void SX126x::set_mode_rx() { this->change_mode_(ModeCommand::MODE_COMMAND_RX); }
|
||||
void SX126x::set_mode_tx() { this->change_mode_(ModeCommand::MODE_COMMAND_TX); }
|
||||
void SX126x::set_mode_standby(SX126xStandbyMode mode) { this->change_mode_(ModeCommand::MODE_COMMAND_STANDBY, mode); }
|
||||
void SX126x::set_mode_sleep(bool cold) {
|
||||
this->change_mode_(cold ? ModeCommand::MODE_COMMAND_COLD_SLEEP : ModeCommand::MODE_COMMAND_SLEEP);
|
||||
}
|
||||
void SX126x::run_image_cal() { this->change_mode_(ModeCommand::MODE_COMMAND_IMAGE_CAL); }
|
||||
|
||||
void SX126x::change_mode_(ModeCommand command, uint8_t arg) {
|
||||
const bool sleep = command == ModeCommand::MODE_COMMAND_SLEEP || command == ModeCommand::MODE_COMMAND_COLD_SLEEP;
|
||||
if (this->cold_sleep_) {
|
||||
if (sleep) {
|
||||
return;
|
||||
}
|
||||
// Cold sleep lost the configuration, so waking needs it applied again first
|
||||
this->configure();
|
||||
}
|
||||
return SX126X_MAX_PACKET_SIZE;
|
||||
if (!this->radio_.can_change_mode()) {
|
||||
ESP_LOGW(TAG, "Radio unavailable, mode change ignored");
|
||||
return;
|
||||
}
|
||||
this->cold_sleep_ = command == ModeCommand::MODE_COMMAND_COLD_SLEEP;
|
||||
this->apply_(this->radio_.change_mode(command, arg));
|
||||
}
|
||||
|
||||
void SX126x::set_packet_params_(uint8_t payload_length) {
|
||||
uint8_t buf[9];
|
||||
if (this->modulation_ == PACKET_TYPE_LORA) {
|
||||
buf[0] = (this->preamble_size_ >> 8) & 0xFF;
|
||||
buf[1] = (this->preamble_size_ >> 0) & 0xFF;
|
||||
buf[2] = (this->payload_length_ > 0) ? 0x01 : 0x00;
|
||||
buf[3] = payload_length;
|
||||
buf[4] = (this->crc_enable_) ? 0x01 : 0x00;
|
||||
buf[5] = 0x00;
|
||||
this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 6);
|
||||
} else {
|
||||
uint16_t preamble_size = this->preamble_size_ * 8;
|
||||
buf[0] = (preamble_size >> 8) & 0xFF;
|
||||
buf[1] = (preamble_size >> 0) & 0xFF;
|
||||
buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00;
|
||||
buf[3] = this->sync_value_.size() * 8;
|
||||
buf[4] = 0x00;
|
||||
buf[5] = (this->payload_length_ > 0) ? 0x00 : 0x01;
|
||||
buf[6] = payload_length;
|
||||
if (this->crc_enable_) {
|
||||
buf[7] = (this->crc_inverted_ ? 0x04 : 0x00) + (this->crc_size_ & 0x02);
|
||||
} else {
|
||||
buf[7] = 0x01;
|
||||
}
|
||||
buf[8] = (this->whitening_enable_) ? 0x01 : 0x00;
|
||||
this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 9);
|
||||
}
|
||||
}
|
||||
size_t SX126x::get_max_packet_size() { return this->radio_.max_packet_size(); }
|
||||
|
||||
SX126xError SX126x::transmit_packet(const uint8_t *data, size_t len) {
|
||||
// Callers such as a template number's on_value can run before setup() or after a failed setup
|
||||
if (!this->is_ready()) {
|
||||
ESP_LOGW(TAG, "Radio not ready, packet dropped");
|
||||
return SX126xError::SX126X_ERROR_NOT_READY;
|
||||
}
|
||||
if (this->payload_length_ > 0 && this->payload_length_ != len) {
|
||||
ESP_LOGE(TAG, "Packet size does not match config");
|
||||
return SX126xError::INVALID_PARAMS;
|
||||
}
|
||||
if (len == 0 || len > this->get_max_packet_size()) {
|
||||
ESP_LOGE(TAG, "Packet size out of range");
|
||||
return SX126xError::INVALID_PARAMS;
|
||||
}
|
||||
|
||||
SX126xError ret = SX126xError::NONE;
|
||||
this->set_mode_standby(STDBY_XOSC);
|
||||
if (this->payload_length_ == 0) {
|
||||
this->set_packet_params_(len);
|
||||
}
|
||||
this->write_fifo_(0x00, data, len);
|
||||
this->set_mode_tx();
|
||||
|
||||
// wait until transmit completes, typically the delay will be less than 100 ms
|
||||
uint32_t start = millis();
|
||||
while (!this->dio1_pin_->digital_read()) {
|
||||
App.feed_wdt();
|
||||
if (millis() - start > TRANSMIT_TIMEOUT_MS) {
|
||||
ESP_LOGE(TAG, "Transmit packet failure");
|
||||
ret = SX126xError::TIMEOUT;
|
||||
break;
|
||||
SX126xError result = this->start_transmit_(nullptr, data, len, TRANSMIT_TIMEOUT_MS);
|
||||
if (result == SX126xError::SX126X_ERROR_NONE) {
|
||||
// Wait for this send's own completion. A send without a listener cannot be cancelled,
|
||||
// and no user code runs between these polls, so the send cannot be replaced.
|
||||
result = SX126xError::SX126X_ERROR_NOT_READY;
|
||||
while (this->radio_.is_transmitting()) {
|
||||
const RadioEffects effects = this->radio_.poll_transmit(millis());
|
||||
if (effects.finished) {
|
||||
result = effects.result;
|
||||
this->apply_(effects);
|
||||
break;
|
||||
}
|
||||
App.feed_wdt();
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t buf[2];
|
||||
buf[0] = 0xFF;
|
||||
buf[1] = 0xFF;
|
||||
this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
|
||||
if (this->payload_length_ == 0) {
|
||||
this->set_packet_params_(this->get_max_packet_size());
|
||||
if (result != SX126xError::SX126X_ERROR_NONE) {
|
||||
ESP_LOGW(
|
||||
TAG, "Packet send failed: %s",
|
||||
LOG_STR_ARG(SX126xErrorStrings::get_log_str(static_cast<uint8_t>(result), SX126xErrorStrings::LAST_INDEX)));
|
||||
}
|
||||
if (this->rx_start_) {
|
||||
this->set_mode_rx();
|
||||
} else {
|
||||
this->set_mode_sleep();
|
||||
}
|
||||
return ret;
|
||||
return result;
|
||||
}
|
||||
|
||||
void SX126x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr) {
|
||||
for (auto &listener : this->listeners_) {
|
||||
listener->on_packet(packet, rssi, snr);
|
||||
SX126xError SX126x::start_transmit_async(SX126xTransmitListener *listener, const uint8_t *data, size_t len,
|
||||
uint32_t timeout_ms) {
|
||||
if (listener == nullptr || timeout_ms == 0 || timeout_ms > MAX_TRANSMIT_TIMEOUT_MS) {
|
||||
return SX126xError::SX126X_ERROR_INVALID_PARAMS;
|
||||
}
|
||||
this->packet_trigger_.trigger(packet, rssi, snr);
|
||||
return this->start_transmit_(listener, data, len, timeout_ms);
|
||||
}
|
||||
|
||||
SX126xError SX126x::start_transmit_(SX126xTransmitListener *listener, const uint8_t *data, size_t len,
|
||||
uint32_t timeout_ms) {
|
||||
if (!this->is_ready()) {
|
||||
return SX126xError::SX126X_ERROR_NOT_READY;
|
||||
}
|
||||
const RadioEffects effects = this->radio_.start_transmit(listener, data, len, timeout_ms);
|
||||
this->apply_(effects);
|
||||
return effects.result;
|
||||
}
|
||||
|
||||
bool SX126x::cancel_transmit_async(SX126xTransmitListener *listener) {
|
||||
const RadioEffects effects = this->radio_.cancel_transmit(listener);
|
||||
if (!effects.finished) {
|
||||
return false;
|
||||
}
|
||||
this->apply_(effects);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SX126x::loop() {
|
||||
if (this->dio1_pin_->is_internal()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
if (!this->dio1_pin_->digital_read()) {
|
||||
if (this->rx_dispatching_ || this->is_failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t status;
|
||||
uint8_t buf[3];
|
||||
uint8_t rssi;
|
||||
int8_t snr;
|
||||
this->read_opcode_(RADIO_GET_IRQSTATUS, buf, 2);
|
||||
this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
|
||||
status = (buf[0] << 8) | buf[1];
|
||||
if ((status & IRQ_RX_DONE) == IRQ_RX_DONE) {
|
||||
if ((status & IRQ_CRC_ERROR) != IRQ_CRC_ERROR) {
|
||||
this->read_opcode_(RADIO_GET_PACKETSTATUS, buf, 3);
|
||||
if (this->modulation_ == PACKET_TYPE_LORA) {
|
||||
rssi = buf[0];
|
||||
snr = buf[1];
|
||||
} else {
|
||||
rssi = buf[2];
|
||||
snr = 0;
|
||||
}
|
||||
this->read_opcode_(RADIO_GET_RXBUFFERSTATUS, buf, 2);
|
||||
this->packet_.resize(buf[0]);
|
||||
this->read_fifo_(buf[1], this->packet_);
|
||||
this->call_listeners_(this->packet_, (float) rssi / -2.0f, (float) snr / 4.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SX126x::run_image_cal() {
|
||||
// the following values were taken from section 9.2.1 table 9-2
|
||||
// in rev 2.1 of the datasheet
|
||||
uint8_t buf[2] = {0, 0};
|
||||
if (this->frequency_ > 900000000) {
|
||||
buf[0] = 0xE1;
|
||||
buf[1] = 0xE9;
|
||||
} else if (this->frequency_ > 850000000) {
|
||||
buf[0] = 0xD7;
|
||||
buf[1] = 0xDB;
|
||||
} else if (this->frequency_ > 770000000) {
|
||||
buf[0] = 0xC1;
|
||||
buf[1] = 0xC5;
|
||||
} else if (this->frequency_ > 460000000) {
|
||||
buf[0] = 0x75;
|
||||
buf[1] = 0x81;
|
||||
} else if (this->frequency_ > 425000000) {
|
||||
buf[0] = 0x6B;
|
||||
buf[1] = 0x6F;
|
||||
}
|
||||
if (buf[0] > 0 && buf[1] > 0) {
|
||||
this->write_opcode_(RADIO_CALIBRATEIMAGE, buf, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void SX126x::set_mode_rx() {
|
||||
uint8_t buf[8];
|
||||
|
||||
// configure irq params
|
||||
uint16_t irq = IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR;
|
||||
buf[0] = (irq >> 8) & 0xFF;
|
||||
buf[1] = (irq >> 0) & 0xFF;
|
||||
buf[2] = (irq >> 8) & 0xFF;
|
||||
buf[3] = (irq >> 0) & 0xFF;
|
||||
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
|
||||
|
||||
// set timeout to 0
|
||||
buf[0] = 0x00;
|
||||
this->write_opcode_(RADIO_SET_LORASYMBTIMEOUT, buf, 1);
|
||||
|
||||
// switch to continuous mode rx
|
||||
buf[0] = 0xFF;
|
||||
buf[1] = 0xFF;
|
||||
buf[2] = 0xFF;
|
||||
this->write_opcode_(RADIO_SET_RX, buf, 3);
|
||||
}
|
||||
|
||||
void SX126x::set_mode_tx() {
|
||||
uint8_t buf[8];
|
||||
|
||||
// configure irq params
|
||||
uint16_t irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT;
|
||||
buf[0] = (irq >> 8) & 0xFF;
|
||||
buf[1] = (irq >> 0) & 0xFF;
|
||||
buf[2] = (irq >> 8) & 0xFF;
|
||||
buf[3] = (irq >> 0) & 0xFF;
|
||||
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
|
||||
|
||||
// switch to single mode tx
|
||||
buf[0] = 0x00;
|
||||
buf[1] = 0x00;
|
||||
buf[2] = 0x00;
|
||||
this->write_opcode_(RADIO_SET_TX, buf, 3);
|
||||
}
|
||||
|
||||
void SX126x::set_mode_sleep(bool cold) {
|
||||
// 0x04 = warm start (config retained), 0x00 = cold start (config lost, lowest power)
|
||||
uint8_t buf[1];
|
||||
buf[0] = cold ? 0x00 : 0x04;
|
||||
this->write_opcode_(RADIO_SET_SLEEP, buf, 1);
|
||||
}
|
||||
|
||||
void SX126x::set_mode_standby(SX126xStandbyMode mode) {
|
||||
uint8_t buf[1];
|
||||
buf[0] = mode;
|
||||
this->write_opcode_(RADIO_SET_STANDBY, buf, 1);
|
||||
}
|
||||
|
||||
void SX126x::wait_busy_() {
|
||||
// wait if the device is busy, the maximum delay is only be a few ms
|
||||
// with most commands taking only a few us
|
||||
uint32_t start = millis();
|
||||
while (this->busy_pin_->digital_read()) {
|
||||
if (millis() - start > BUSY_TIMEOUT_MS) {
|
||||
this->mark_failed(LOG_STR("Wait busy timeout"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
this->apply_(this->radio_.poll(App.get_loop_component_start_time(), this->packet_));
|
||||
}
|
||||
|
||||
void SX126x::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "SX126x:");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_PIN(" BUSY Pin: ", this->busy_pin_);
|
||||
LOG_PIN(" RST Pin: ", this->rst_pin_);
|
||||
LOG_PIN(" DIO1 Pin: ", this->dio1_pin_);
|
||||
ESP_LOGCONFIG(TAG, "SX126x:\n Ready for operation: %s",
|
||||
YESNO(!this->is_failed() && this->radio_.state() == RadioState::RADIO_STATE_IDLE));
|
||||
LOG_PIN(" CS Pin: ", this->radio_.bus().get_cs_pin());
|
||||
LOG_PIN(" BUSY Pin: ", this->radio_.bus().get_busy_pin());
|
||||
LOG_PIN(" RST Pin: ", this->radio_.get_rst_pin());
|
||||
LOG_PIN(" DIO1 Pin: ", this->radio_.get_dio1_pin());
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" HW Version: %15s\n"
|
||||
" Requested configuration:\n"
|
||||
" Frequency: %" PRIu32 " Hz\n"
|
||||
" Bandwidth: %" PRIu32 " Hz\n"
|
||||
" PA Power: %" PRId8 " dBm\n"
|
||||
" PA Ramp: %" PRIu16 " us\n"
|
||||
" Payload Length: %" PRIu32 "\n"
|
||||
" Payload Length: %" PRIu8 "\n"
|
||||
" CRC Enable: %s\n"
|
||||
" Rx Start: %s",
|
||||
this->version_, this->frequency_, BW_HZ[this->bandwidth_], this->pa_power_,
|
||||
progmem_read_uint16(&RAMP[this->pa_ramp_]), this->payload_length_, TRUEFALSE(this->crc_enable_),
|
||||
TRUEFALSE(this->rx_start_));
|
||||
if (this->modulation_ == PACKET_TYPE_GFSK) {
|
||||
this->version_, this->requested_profile_.frequency,
|
||||
SX126xCommands::bandwidth_hz(this->requested_profile_.bandwidth), this->requested_profile_.pa_power,
|
||||
SX126xCommands::ramp_us(this->requested_profile_.pa_ramp), this->requested_profile_.payload_length,
|
||||
TRUEFALSE(this->requested_profile_.crc_enable), TRUEFALSE(this->requested_profile_.rx_start));
|
||||
if (this->requested_profile_.modulation == PACKET_TYPE_GFSK) {
|
||||
const char *shaping = "NONE";
|
||||
if (this->shaping_ == GAUSSIAN_BT_0_3) {
|
||||
if (this->requested_profile_.shaping == GAUSSIAN_BT_0_3) {
|
||||
shaping = "GAUSSIAN_BT_0_3";
|
||||
} else if (this->shaping_ == GAUSSIAN_BT_0_5) {
|
||||
} else if (this->requested_profile_.shaping == GAUSSIAN_BT_0_5) {
|
||||
shaping = "GAUSSIAN_BT_0_5";
|
||||
} else if (this->shaping_ == GAUSSIAN_BT_0_7) {
|
||||
} else if (this->requested_profile_.shaping == GAUSSIAN_BT_0_7) {
|
||||
shaping = "GAUSSIAN_BT_0_7";
|
||||
} else if (this->shaping_ == GAUSSIAN_BT_1_0) {
|
||||
} else if (this->requested_profile_.shaping == GAUSSIAN_BT_1_0) {
|
||||
shaping = "GAUSSIAN_BT_1_0";
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
@@ -539,14 +222,15 @@ void SX126x::dump_config() {
|
||||
" Preamble Size: %" PRIu16 "\n"
|
||||
" Preamble Detect: %" PRIu16 "\n"
|
||||
" Bitrate: %" PRIu32 "b/s",
|
||||
this->deviation_, shaping, this->preamble_size_, this->preamble_detect_, this->bitrate_);
|
||||
} else if (this->modulation_ == PACKET_TYPE_LORA) {
|
||||
this->requested_profile_.deviation, shaping, this->requested_profile_.preamble_size,
|
||||
this->requested_profile_.preamble_detect, this->requested_profile_.bitrate);
|
||||
} else if (this->requested_profile_.modulation == PACKET_TYPE_LORA) {
|
||||
const char *cr = "4/8";
|
||||
if (this->coding_rate_ == LORA_CR_4_5) {
|
||||
if (this->requested_profile_.coding_rate == LORA_CR_4_5) {
|
||||
cr = "4/5";
|
||||
} else if (this->coding_rate_ == LORA_CR_4_6) {
|
||||
} else if (this->requested_profile_.coding_rate == LORA_CR_4_6) {
|
||||
cr = "4/6";
|
||||
} else if (this->coding_rate_ == LORA_CR_4_7) {
|
||||
} else if (this->requested_profile_.coding_rate == LORA_CR_4_7) {
|
||||
cr = "4/7";
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
@@ -554,16 +238,17 @@ void SX126x::dump_config() {
|
||||
" Spreading Factor: %" PRIu8 "\n"
|
||||
" Coding Rate: %s\n"
|
||||
" Preamble Size: %" PRIu16,
|
||||
this->spreading_factor_, cr, this->preamble_size_);
|
||||
this->requested_profile_.spreading_factor, cr, this->requested_profile_.preamble_size);
|
||||
}
|
||||
if (!this->sync_value_.empty()) {
|
||||
if (this->requested_profile_.sync_size != 0) {
|
||||
char hex_buf[17]; // 8 bytes max = 16 hex chars + null
|
||||
ESP_LOGCONFIG(TAG, " Sync Value: 0x%s",
|
||||
format_hex_to(hex_buf, this->sync_value_.data(), this->sync_value_.size()));
|
||||
ESP_LOGCONFIG(
|
||||
TAG, " Sync Value: 0x%s",
|
||||
format_hex_to(hex_buf, this->requested_profile_.sync_value.data(), this->requested_profile_.sync_size));
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Whitening Enable: %s", TRUEFALSE(this->whitening_enable_));
|
||||
if (this->whitening_enable_) {
|
||||
ESP_LOGCONFIG(TAG, " Whitening Initial: 0x%03x", this->whitening_initial_);
|
||||
ESP_LOGCONFIG(TAG, " Whitening Enable: %s", TRUEFALSE(this->requested_profile_.whitening_enable));
|
||||
if (this->requested_profile_.whitening_enable) {
|
||||
ESP_LOGCONFIG(TAG, " Whitening Initial: 0x%03x", this->requested_profile_.whitening_initial);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,157 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include "sx126x_radio.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "sx126x_reg.h"
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
static constexpr size_t SX126X_MAX_PACKET_SIZE = 255;
|
||||
|
||||
enum SX126xBw : uint8_t {
|
||||
// FSK
|
||||
SX126X_BW_4800,
|
||||
SX126X_BW_5800,
|
||||
SX126X_BW_7300,
|
||||
SX126X_BW_9700,
|
||||
SX126X_BW_11700,
|
||||
SX126X_BW_14600,
|
||||
SX126X_BW_19500,
|
||||
SX126X_BW_23400,
|
||||
SX126X_BW_29300,
|
||||
SX126X_BW_39000,
|
||||
SX126X_BW_46900,
|
||||
SX126X_BW_58600,
|
||||
SX126X_BW_78200,
|
||||
SX126X_BW_93800,
|
||||
SX126X_BW_117300,
|
||||
SX126X_BW_156200,
|
||||
SX126X_BW_187200,
|
||||
SX126X_BW_234300,
|
||||
SX126X_BW_312000,
|
||||
SX126X_BW_373600,
|
||||
SX126X_BW_467000,
|
||||
// LORA
|
||||
SX126X_BW_7810,
|
||||
SX126X_BW_10420,
|
||||
SX126X_BW_15630,
|
||||
SX126X_BW_20830,
|
||||
SX126X_BW_31250,
|
||||
SX126X_BW_41670,
|
||||
SX126X_BW_62500,
|
||||
SX126X_BW_125000,
|
||||
SX126X_BW_250000,
|
||||
SX126X_BW_500000,
|
||||
};
|
||||
|
||||
enum class SX126xError { NONE = 0, TIMEOUT, INVALID_PARAMS, SX126X_ERROR_NOT_READY };
|
||||
|
||||
class SX126xListener {
|
||||
public:
|
||||
// Called from loop(). The packet is borrowed until this returns; copy what you keep.
|
||||
// Listeners may send, change mode or configure. A send returns BUSY if another packet
|
||||
// is already waiting, which is then delivered by the next loop().
|
||||
virtual void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) = 0;
|
||||
};
|
||||
|
||||
class SX126x final : public Component,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
spi::DATA_RATE_8MHZ> {
|
||||
class SX126x final : public Component {
|
||||
public:
|
||||
void set_spi_parent(spi::SPIComponent *parent) { this->radio_.bus().set_spi_parent(parent); }
|
||||
void set_cs_pin(GPIOPin *pin) { this->radio_.bus().set_cs_pin(pin); }
|
||||
void set_data_rate(uint32_t rate) { this->radio_.bus().set_data_rate(rate); }
|
||||
void set_mode(spi::SPIMode mode) { this->radio_.bus().set_mode(mode); }
|
||||
void set_bit_order(spi::SPIBitOrder order) { this->radio_.bus().set_bit_order(order); }
|
||||
void set_release_device(bool release) { this->radio_.bus().set_release_device(release); }
|
||||
void set_write_only(bool write_only) { this->radio_.bus().set_write_only(write_only); }
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
void set_psram_dma(bool enable) { this->radio_.bus().set_psram_dma(enable); }
|
||||
#endif
|
||||
// Releases the SPI bus, as SPIDevice::spi_teardown() did before SX126x stopped being one
|
||||
void spi_teardown() { this->radio_.bus().teardown(); }
|
||||
size_t get_max_packet_size();
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void set_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; }
|
||||
void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; }
|
||||
void set_busy_pin(GPIOPin *busy_pin) { this->busy_pin_ = busy_pin; }
|
||||
void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; }
|
||||
void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; }
|
||||
void set_crc_inverted(bool crc_inverted) { this->crc_inverted_ = crc_inverted; }
|
||||
void set_crc_size(uint8_t crc_size) { this->crc_size_ = crc_size; }
|
||||
void set_crc_polynomial(uint16_t crc_polynomial) { this->crc_polynomial_ = crc_polynomial; }
|
||||
void set_crc_initial(uint16_t crc_initial) { this->crc_initial_ = crc_initial; }
|
||||
void set_whitening_enable(bool whitening_enable) { this->whitening_enable_ = whitening_enable; }
|
||||
void set_whitening_initial(uint16_t whitening_initial) { this->whitening_initial_ = whitening_initial; }
|
||||
void set_deviation(uint32_t deviation) { this->deviation_ = deviation; }
|
||||
void set_dio1_pin(GPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; }
|
||||
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
|
||||
void set_sx1261(bool sx1261) { this->sx1261_ = sx1261; }
|
||||
void set_bandwidth(SX126xBw bandwidth) { this->requested_profile_.bandwidth = bandwidth; }
|
||||
void set_bitrate(uint32_t bitrate) { this->requested_profile_.bitrate = bitrate; }
|
||||
void set_busy_pin(GPIOPin *busy_pin) { this->radio_.bus().set_busy_pin(busy_pin); }
|
||||
void set_coding_rate(uint8_t coding_rate) { this->requested_profile_.coding_rate = coding_rate; }
|
||||
void set_crc_enable(bool crc_enable) { this->requested_profile_.crc_enable = crc_enable; }
|
||||
void set_crc_inverted(bool crc_inverted) { this->requested_profile_.crc_inverted = crc_inverted; }
|
||||
void set_crc_size(uint8_t crc_size) { this->requested_profile_.crc_size = crc_size; }
|
||||
void set_crc_polynomial(uint16_t crc_polynomial) { this->requested_profile_.crc_polynomial = crc_polynomial; }
|
||||
void set_crc_initial(uint16_t crc_initial) { this->requested_profile_.crc_initial = crc_initial; }
|
||||
void set_whitening_enable(bool whitening_enable) { this->requested_profile_.whitening_enable = whitening_enable; }
|
||||
void set_whitening_initial(uint16_t whitening_initial) {
|
||||
this->requested_profile_.whitening_initial = whitening_initial;
|
||||
}
|
||||
void set_deviation(uint32_t deviation) { this->requested_profile_.deviation = deviation; }
|
||||
void set_dio1_pin(GPIOPin *dio1_pin) { this->radio_.set_dio1_pin(dio1_pin); }
|
||||
void set_frequency(uint32_t frequency) { this->requested_profile_.frequency = frequency; }
|
||||
void set_sx1261(bool sx1261) { this->requested_profile_.sx1261 = sx1261; }
|
||||
// Mode changes are ignored with a warning unless the radio is idle and configured.
|
||||
void set_mode_rx();
|
||||
void set_mode_tx();
|
||||
void set_mode_standby(SX126xStandbyMode mode);
|
||||
void set_mode_sleep(bool cold = false);
|
||||
void set_modulation(uint8_t modulation) { this->modulation_ = modulation; }
|
||||
void set_pa_power(int8_t power) { this->pa_power_ = power; }
|
||||
void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; }
|
||||
void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; }
|
||||
void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; }
|
||||
void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; }
|
||||
void set_rst_pin(GPIOPin *rst_pin) { this->rst_pin_ = rst_pin; }
|
||||
void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; }
|
||||
void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; }
|
||||
void set_shaping(uint8_t shaping) { this->shaping_ = shaping; }
|
||||
void set_spreading_factor(uint8_t spreading_factor) { this->spreading_factor_ = spreading_factor; }
|
||||
void set_sync_value(const std::vector<uint8_t> &sync_value) { this->sync_value_ = sync_value; }
|
||||
void set_tcxo_voltage(uint8_t tcxo_voltage) { this->tcxo_voltage_ = tcxo_voltage; }
|
||||
void set_tcxo_delay(uint32_t tcxo_delay) { this->tcxo_delay_ = tcxo_delay; }
|
||||
void set_modulation(uint8_t modulation) { this->requested_profile_.modulation = modulation; }
|
||||
void set_pa_power(int8_t power) { this->requested_profile_.pa_power = power; }
|
||||
void set_pa_ramp(uint8_t ramp) { this->requested_profile_.pa_ramp = ramp; }
|
||||
void set_payload_length(uint8_t payload_length) { this->requested_profile_.payload_length = payload_length; }
|
||||
void set_preamble_detect(uint16_t preamble_detect) { this->requested_profile_.preamble_detect = preamble_detect; }
|
||||
void set_preamble_size(uint16_t preamble_size) { this->requested_profile_.preamble_size = preamble_size; }
|
||||
void set_rst_pin(GPIOPin *rst_pin) { this->radio_.set_rst_pin(rst_pin); }
|
||||
void set_rx_start(bool rx_start) { this->requested_profile_.rx_start = rx_start; }
|
||||
void set_rf_switch(bool rf_switch) { this->requested_profile_.rf_switch = rf_switch; }
|
||||
void set_shaping(uint8_t shaping) { this->requested_profile_.shaping = shaping; }
|
||||
void set_spreading_factor(uint8_t spreading_factor) { this->requested_profile_.spreading_factor = spreading_factor; }
|
||||
void set_sync_value(const std::vector<uint8_t> &sync_value) { this->requested_profile_.set_sync_value(sync_value); }
|
||||
void set_tcxo_voltage(uint8_t tcxo_voltage) { this->requested_profile_.tcxo_voltage = tcxo_voltage; }
|
||||
void set_tcxo_delay(uint32_t tcxo_delay) { this->requested_profile_.tcxo_delay = tcxo_delay; }
|
||||
void run_image_cal();
|
||||
// Mode changes and configuration are allowed during RX callbacks when the radio
|
||||
// is available; they never modify the borrowed packet buffer.
|
||||
void configure();
|
||||
SX126xError transmit_packet(const uint8_t *data, size_t len);
|
||||
SX126xError transmit_packet(const std::vector<uint8_t> &packet) {
|
||||
return this->transmit_packet(packet.data(), packet.size());
|
||||
}
|
||||
// Main-loop context only. SX126X_ERROR_NONE accepts one operation and promises one completion.
|
||||
// Data is copied into the chip FIFO before return; listener must outlive completion.
|
||||
// Pending RX rejects admission with BUSY and is delivered by a later loop().
|
||||
// Timeout: 1..262143 ms. SPI/BUSY waits are synchronous; airtime is asynchronous.
|
||||
[[nodiscard]] SX126xError start_transmit_async(SX126xTransmitListener *listener, const uint8_t *data, size_t len,
|
||||
uint32_t timeout_ms = 4000);
|
||||
[[nodiscard]] SX126xError start_transmit_async(SX126xTransmitListener *listener, const std::vector<uint8_t> &packet,
|
||||
uint32_t timeout_ms = 4000) {
|
||||
return this->start_transmit_async(listener, packet.data(), packet.size(), timeout_ms);
|
||||
}
|
||||
// Normal completion runs in loop(); cancellation completes synchronously in its caller.
|
||||
// The radio is released before callbacks, which may start another operation if recovery
|
||||
// succeeded. True accepts cancellation; the completion result reports cleanup failure.
|
||||
bool cancel_transmit_async(SX126xTransmitListener *listener);
|
||||
bool is_transmitting() const { return this->radio_.is_transmitting(); }
|
||||
void register_listener(SX126xListener *listener) { this->listeners_.push_back(listener); }
|
||||
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
static void gpio_intr(SX126x *arg);
|
||||
void configure_fsk_ook_();
|
||||
void configure_lora_();
|
||||
void set_packet_params_(uint8_t payload_length);
|
||||
uint8_t read_fifo_(uint8_t offset, std::vector<uint8_t> &packet);
|
||||
void write_fifo_(uint8_t offset, const uint8_t *data, size_t len);
|
||||
void write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
|
||||
uint8_t read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
|
||||
void write_register_(uint16_t reg, uint8_t *data, uint8_t size);
|
||||
void read_register_(uint16_t reg, uint8_t *data, uint8_t size);
|
||||
void call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr);
|
||||
void wait_busy_();
|
||||
private:
|
||||
static void IRAM_ATTR gpio_intr(SX126x *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void change_mode_(ModeCommand command, uint8_t arg = 0);
|
||||
// The only place that runs user code for a radio operation: loop scheduling, then status,
|
||||
// then the completion or packet listeners. SX126xRadio has already committed its state.
|
||||
void apply_(const RadioEffects &effects);
|
||||
void watch_configuration_();
|
||||
SX126xError start_transmit_(SX126xTransmitListener *listener, const uint8_t *data, size_t len, uint32_t timeout_ms);
|
||||
SX126xRadio radio_;
|
||||
RadioProfile requested_profile_;
|
||||
char version_[16]{};
|
||||
Trigger<std::vector<uint8_t>, float, float> packet_trigger_;
|
||||
std::vector<SX126xListener *> listeners_;
|
||||
std::vector<uint8_t> packet_;
|
||||
std::vector<uint8_t> sync_value_;
|
||||
GPIOPin *busy_pin_{nullptr};
|
||||
GPIOPin *dio1_pin_{nullptr};
|
||||
GPIOPin *rst_pin_{nullptr};
|
||||
uint32_t bitrate_{0};
|
||||
uint32_t deviation_{0};
|
||||
uint32_t frequency_{0};
|
||||
uint32_t payload_length_{0};
|
||||
uint32_t tcxo_delay_{0};
|
||||
uint16_t crc_polynomial_{0};
|
||||
uint16_t crc_initial_{0};
|
||||
uint16_t whitening_initial_{0};
|
||||
uint16_t preamble_detect_{0};
|
||||
uint16_t preamble_size_{0};
|
||||
char version_[16];
|
||||
SX126xBw bandwidth_{SX126X_BW_125000};
|
||||
uint8_t crc_size_{0};
|
||||
uint8_t tcxo_voltage_{0};
|
||||
uint8_t coding_rate_{0};
|
||||
uint8_t modulation_{PACKET_TYPE_LORA};
|
||||
uint8_t pa_ramp_{0};
|
||||
uint8_t shaping_{0};
|
||||
uint8_t spreading_factor_{0};
|
||||
int8_t pa_power_{0};
|
||||
bool sx1261_{false};
|
||||
bool crc_enable_{false};
|
||||
bool crc_inverted_{false};
|
||||
bool whitening_enable_{false};
|
||||
bool rx_start_{false};
|
||||
bool rf_switch_{false};
|
||||
bool rx_dispatching_{false};
|
||||
bool cold_sleep_{false};
|
||||
bool reconfigure_pending_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -0,0 +1,355 @@
|
||||
#include "sx126x_commands.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
static constexpr uint16_t RAMP[8] PROGMEM = {10, 20, 40, 80, 200, 800, 1700, 3400};
|
||||
// Aligned 32 bit loads from flash are safe on ESP8266, so this table is read directly
|
||||
static constexpr uint32_t BW_HZ[31] PROGMEM = {4800, 5800, 7300, 9700, 11700, 14600, 19500, 23400,
|
||||
29300, 39000, 46900, 58600, 78200, 93800, 117300, 156200,
|
||||
187200, 234300, 312000, 373600, 467000, 7810, 10420, 15630,
|
||||
20830, 31250, 41670, 62500, 125000, 250000, 500000};
|
||||
static constexpr uint8_t BW_LORA[10] PROGMEM = {LORA_BW_7810, LORA_BW_10420, LORA_BW_15630, LORA_BW_20830,
|
||||
LORA_BW_31250, LORA_BW_41670, LORA_BW_62500, LORA_BW_125000,
|
||||
LORA_BW_250000, LORA_BW_500000};
|
||||
static constexpr uint8_t BW_FSK[21] PROGMEM = {
|
||||
FSK_BW_4800, FSK_BW_5800, FSK_BW_7300, FSK_BW_9700, FSK_BW_11700, FSK_BW_14600, FSK_BW_19500,
|
||||
FSK_BW_23400, FSK_BW_29300, FSK_BW_39000, FSK_BW_46900, FSK_BW_58600, FSK_BW_78200, FSK_BW_93800,
|
||||
FSK_BW_117300, FSK_BW_156200, FSK_BW_187200, FSK_BW_234300, FSK_BW_312000, FSK_BW_373600, FSK_BW_467000};
|
||||
|
||||
// OCP (Over Current Protection) values
|
||||
static constexpr uint8_t OCP_80MA = 0x18; // 80 mA max current
|
||||
static constexpr uint8_t OCP_140MA = 0x38; // 140 mA max current
|
||||
|
||||
// LoRa low data rate optimization threshold
|
||||
static constexpr float LOW_DATA_RATE_OPTIMIZE_THRESHOLD = 16.38f; // 16.38 ms
|
||||
|
||||
void SX126xCommands::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
|
||||
const uint8_t header[] = {opcode, 0};
|
||||
this->spi_device_.read(header, {data, size});
|
||||
}
|
||||
|
||||
void SX126xCommands::write_opcode_(uint8_t opcode, const uint8_t *data, uint8_t size) {
|
||||
this->spi_device_.write({&opcode, 1}, {data, size});
|
||||
}
|
||||
|
||||
void SX126xCommands::read_register_(uint16_t reg, uint8_t *data, uint8_t size) {
|
||||
const uint8_t header[] = {RADIO_READ_REGISTER, static_cast<uint8_t>(reg >> 8), static_cast<uint8_t>(reg), 0};
|
||||
this->spi_device_.read(header, {data, size});
|
||||
}
|
||||
|
||||
void SX126xCommands::write_register_(uint16_t reg, const uint8_t *data, uint8_t size) {
|
||||
const uint8_t header[] = {RADIO_WRITE_REGISTER, static_cast<uint8_t>(reg >> 8), static_cast<uint8_t>(reg)};
|
||||
this->spi_device_.write(header, {data, size});
|
||||
}
|
||||
|
||||
ConfigurationResult SX126xCommands::configure(const RadioProfile &profile, std::span<char, 16> version,
|
||||
AppliedProfile &applied) {
|
||||
applied.frequency = profile.frequency;
|
||||
applied.payload_length = profile.payload_length;
|
||||
applied.modulation = profile.modulation;
|
||||
applied.rx_start = profile.rx_start;
|
||||
encode_packet_params(profile, applied);
|
||||
uint8_t buf[8];
|
||||
// wakeup
|
||||
this->read_opcode_(RADIO_GET_STATUS, nullptr, 0);
|
||||
|
||||
// config tcxo
|
||||
if (profile.tcxo_voltage != TCXO_CTRL_NONE) {
|
||||
uint32_t delay = profile.tcxo_delay >> 6;
|
||||
buf[0] = profile.tcxo_voltage;
|
||||
buf[1] = (delay >> 16) & 0xFF;
|
||||
buf[2] = (delay >> 8) & 0xFF;
|
||||
buf[3] = (delay >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_TCXOMODE, buf, 4);
|
||||
buf[0] = 0x7F;
|
||||
this->write_opcode_(RADIO_CALIBRATE, buf, 1);
|
||||
}
|
||||
|
||||
// clear errors
|
||||
buf[0] = 0x00;
|
||||
buf[1] = 0x00;
|
||||
this->write_opcode_(RADIO_CLR_ERROR, buf, 2);
|
||||
|
||||
// rf switch
|
||||
if (profile.rf_switch) {
|
||||
buf[0] = 0x01;
|
||||
this->write_opcode_(RADIO_SET_RFSWITCHMODE, buf, 1);
|
||||
}
|
||||
|
||||
// check silicon version to make sure hw is ok
|
||||
this->read_register_(REG_VERSION_STRING, reinterpret_cast<uint8_t *>(version.data()), version.size());
|
||||
if (!this->spi_device_.ok()) {
|
||||
return ConfigurationResult::CONFIGURATION_RESULT_BUSY_TIMEOUT;
|
||||
}
|
||||
version[version.size() - 1] = '\0';
|
||||
if (strncmp(version.data(), "SX126", 5) != 0 && strncmp(version.data(), "LLCC68", 6) != 0) {
|
||||
return ConfigurationResult::CONFIGURATION_RESULT_UNSUPPORTED;
|
||||
}
|
||||
|
||||
// setup packet type
|
||||
buf[0] = profile.modulation;
|
||||
this->write_opcode_(RADIO_SET_PACKETTYPE, buf, 1);
|
||||
|
||||
// calibrate image
|
||||
this->calibrate_image(profile.frequency);
|
||||
|
||||
// set frequency
|
||||
uint64_t freq = ((uint64_t) profile.frequency << 25) / XTAL_FREQ;
|
||||
buf[0] = (uint8_t) ((freq >> 24) & 0xFF);
|
||||
buf[1] = (uint8_t) ((freq >> 16) & 0xFF);
|
||||
buf[2] = (uint8_t) ((freq >> 8) & 0xFF);
|
||||
buf[3] = (uint8_t) (freq & 0xFF);
|
||||
this->write_opcode_(RADIO_SET_RFFREQUENCY, buf, 4);
|
||||
|
||||
// configure pa
|
||||
int8_t pa_power = profile.pa_power;
|
||||
if (profile.sx1261) {
|
||||
// the following values were taken from section 13.1.14.1 table 13-21
|
||||
// in rev 2.1 of the datasheet
|
||||
if (pa_power == 15) {
|
||||
uint8_t cfg[4] = {0x06, 0x00, 0x01, 0x01};
|
||||
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
|
||||
} else {
|
||||
uint8_t cfg[4] = {0x04, 0x00, 0x01, 0x01};
|
||||
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
|
||||
}
|
||||
pa_power = clamp(pa_power, (int8_t) -3, (int8_t) 14);
|
||||
buf[0] = OCP_80MA;
|
||||
this->write_register_(REG_OCP, buf, 1);
|
||||
} else {
|
||||
// the following values were taken from section 13.1.14.1 table 13-21
|
||||
// in rev 2.1 of the datasheet
|
||||
uint8_t cfg[4] = {0x04, 0x07, 0x00, 0x01};
|
||||
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
|
||||
pa_power = clamp(pa_power, (int8_t) -3, (int8_t) 22);
|
||||
buf[0] = OCP_140MA;
|
||||
this->write_register_(REG_OCP, buf, 1);
|
||||
}
|
||||
buf[0] = pa_power;
|
||||
buf[1] = profile.pa_ramp;
|
||||
this->write_opcode_(RADIO_SET_TXPARAMS, buf, 2);
|
||||
|
||||
// configure modem
|
||||
if (profile.modulation == PACKET_TYPE_LORA) {
|
||||
// set modulation params
|
||||
float duration = 1000.0f * (1UL << profile.spreading_factor) / BW_HZ[profile.bandwidth];
|
||||
buf[0] = profile.spreading_factor;
|
||||
buf[1] = progmem_read_byte(&BW_LORA[profile.bandwidth - SX126X_BW_7810]);
|
||||
buf[2] = profile.coding_rate;
|
||||
buf[3] = (duration > LOW_DATA_RATE_OPTIMIZE_THRESHOLD) ? 0x01 : 0x00;
|
||||
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4);
|
||||
|
||||
// set packet params and sync word
|
||||
this->write_opcode_(RADIO_SET_PACKETPARAMS, applied.packet_params.data(), applied.packet_params_size);
|
||||
if (profile.sync_size == 2) {
|
||||
this->write_register_(REG_LORA_SYNCWORD, profile.sync_value.data(), profile.sync_size);
|
||||
}
|
||||
} else {
|
||||
// set modulation params
|
||||
uint32_t bitrate = ((uint64_t) XTAL_FREQ * 32) / profile.bitrate;
|
||||
uint32_t fdev = ((uint64_t) profile.deviation << 25) / XTAL_FREQ;
|
||||
buf[0] = (bitrate >> 16) & 0xFF;
|
||||
buf[1] = (bitrate >> 8) & 0xFF;
|
||||
buf[2] = (bitrate >> 0) & 0xFF;
|
||||
buf[3] = profile.shaping;
|
||||
buf[4] = progmem_read_byte(&BW_FSK[profile.bandwidth - SX126X_BW_4800]);
|
||||
buf[5] = (fdev >> 16) & 0xFF;
|
||||
buf[6] = (fdev >> 8) & 0xFF;
|
||||
buf[7] = (fdev >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8);
|
||||
|
||||
// set crc params
|
||||
if (profile.crc_enable) {
|
||||
buf[0] = profile.crc_initial >> 8;
|
||||
buf[1] = profile.crc_initial & 0xFF;
|
||||
this->write_register_(REG_CRC_INITIAL, buf, 2);
|
||||
buf[0] = profile.crc_polynomial >> 8;
|
||||
buf[1] = profile.crc_polynomial & 0xFF;
|
||||
this->write_register_(REG_CRC_POLYNOMIAL, buf, 2);
|
||||
}
|
||||
|
||||
// set whitening params
|
||||
if (profile.whitening_enable) {
|
||||
// according to the datasheet, section 12 table 12-1 "The user should not
|
||||
// change the value of the 7 MSB of this register"
|
||||
this->read_register_(REG_WHITENING_INITIAL, buf, 1);
|
||||
buf[0] = (buf[0] & 0xFE) | ((profile.whitening_initial >> 8) & 0x01);
|
||||
buf[1] = profile.whitening_initial & 0xFF;
|
||||
this->write_register_(REG_WHITENING_INITIAL, buf, 2);
|
||||
}
|
||||
|
||||
// set packet params and sync word
|
||||
this->write_opcode_(RADIO_SET_PACKETPARAMS, applied.packet_params.data(), applied.packet_params_size);
|
||||
if (profile.sync_size != 0) {
|
||||
this->write_register_(REG_GFSK_SYNCWORD, profile.sync_value.data(), profile.sync_size);
|
||||
}
|
||||
}
|
||||
|
||||
// switch to rx or sleep
|
||||
if (profile.rx_start) {
|
||||
this->receive();
|
||||
} else {
|
||||
this->sleep();
|
||||
}
|
||||
return this->spi_device_.ok() ? ConfigurationResult::CONFIGURATION_RESULT_OK
|
||||
: ConfigurationResult::CONFIGURATION_RESULT_BUSY_TIMEOUT;
|
||||
}
|
||||
|
||||
void SX126xCommands::encode_packet_params(const RadioProfile &profile, AppliedProfile &applied) {
|
||||
auto &buf = applied.packet_params;
|
||||
const uint8_t payload_length = profile.max_packet_size();
|
||||
if (profile.modulation == PACKET_TYPE_LORA) {
|
||||
buf[0] = (profile.preamble_size >> 8) & 0xFF;
|
||||
buf[1] = (profile.preamble_size >> 0) & 0xFF;
|
||||
buf[2] = (profile.payload_length > 0) ? 0x01 : 0x00;
|
||||
buf[3] = payload_length;
|
||||
buf[4] = (profile.crc_enable) ? 0x01 : 0x00;
|
||||
buf[5] = 0x00;
|
||||
applied.packet_params_size = 6;
|
||||
} else {
|
||||
uint16_t preamble_size = profile.preamble_size * 8;
|
||||
buf[0] = (preamble_size >> 8) & 0xFF;
|
||||
buf[1] = (preamble_size >> 0) & 0xFF;
|
||||
buf[2] = (profile.preamble_detect > 0) ? ((profile.preamble_detect - 1) | 0x04) : 0x00;
|
||||
buf[3] = profile.sync_size * 8;
|
||||
buf[4] = 0x00;
|
||||
buf[5] = (profile.payload_length > 0) ? 0x00 : 0x01;
|
||||
buf[6] = payload_length;
|
||||
if (profile.crc_enable) {
|
||||
buf[7] = (profile.crc_inverted ? 0x04 : 0x00) + (profile.crc_size & 0x02);
|
||||
} else {
|
||||
buf[7] = 0x01;
|
||||
}
|
||||
buf[8] = (profile.whitening_enable) ? 0x01 : 0x00;
|
||||
applied.packet_params_size = 9;
|
||||
}
|
||||
}
|
||||
|
||||
void SX126xCommands::set_packet_length(AppliedProfile &applied, uint8_t length) {
|
||||
applied.packet_params[applied.modulation == PACKET_TYPE_LORA ? 3 : 6] = length;
|
||||
this->write_opcode_(RADIO_SET_PACKETPARAMS, applied.packet_params.data(), applied.packet_params_size);
|
||||
}
|
||||
|
||||
void SX126xCommands::calibrate_image(uint32_t frequency) {
|
||||
// the following values were taken from section 9.2.1 table 9-2
|
||||
// in rev 2.1 of the datasheet
|
||||
uint8_t buf[2] = {0, 0};
|
||||
if (frequency > 900000000) {
|
||||
buf[0] = 0xE1;
|
||||
buf[1] = 0xE9;
|
||||
} else if (frequency > 850000000) {
|
||||
buf[0] = 0xD7;
|
||||
buf[1] = 0xDB;
|
||||
} else if (frequency > 770000000) {
|
||||
buf[0] = 0xC1;
|
||||
buf[1] = 0xC5;
|
||||
} else if (frequency > 460000000) {
|
||||
buf[0] = 0x75;
|
||||
buf[1] = 0x81;
|
||||
} else if (frequency > 425000000) {
|
||||
buf[0] = 0x6B;
|
||||
buf[1] = 0x6F;
|
||||
}
|
||||
if (buf[0] > 0 && buf[1] > 0) {
|
||||
this->write_opcode_(RADIO_CALIBRATEIMAGE, buf, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void SX126xCommands::receive() {
|
||||
uint8_t buf[8];
|
||||
|
||||
// configure irq params
|
||||
uint16_t irq = IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR;
|
||||
buf[0] = (irq >> 8) & 0xFF;
|
||||
buf[1] = (irq >> 0) & 0xFF;
|
||||
buf[2] = (irq >> 8) & 0xFF;
|
||||
buf[3] = (irq >> 0) & 0xFF;
|
||||
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
|
||||
|
||||
// set timeout to 0
|
||||
buf[0] = 0x00;
|
||||
this->write_opcode_(RADIO_SET_LORASYMBTIMEOUT, buf, 1);
|
||||
|
||||
// switch to continuous mode rx
|
||||
buf[0] = 0xFF;
|
||||
buf[1] = 0xFF;
|
||||
buf[2] = 0xFF;
|
||||
this->write_opcode_(RADIO_SET_RX, buf, 3);
|
||||
}
|
||||
|
||||
void SX126xCommands::transmit(uint32_t timeout_ms) {
|
||||
uint8_t buf[8];
|
||||
|
||||
// configure irq params
|
||||
uint16_t irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT;
|
||||
buf[0] = (irq >> 8) & 0xFF;
|
||||
buf[1] = (irq >> 0) & 0xFF;
|
||||
buf[2] = (irq >> 8) & 0xFF;
|
||||
buf[3] = (irq >> 0) & 0xFF;
|
||||
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
|
||||
|
||||
const uint32_t ticks = timeout_ms * 64;
|
||||
buf[0] = ticks >> 16;
|
||||
buf[1] = ticks >> 8;
|
||||
buf[2] = ticks;
|
||||
this->write_opcode_(RADIO_SET_TX, buf, 3);
|
||||
}
|
||||
|
||||
void SX126xCommands::sleep(bool cold) {
|
||||
// 0x04 = warm start (config retained), 0x00 = cold start (config lost, lowest power)
|
||||
const uint8_t mode = cold ? 0x00 : 0x04;
|
||||
this->write_opcode_(RADIO_SET_SLEEP, &mode, 1);
|
||||
}
|
||||
|
||||
void SX126xCommands::standby(SX126xStandbyMode mode) {
|
||||
const uint8_t value = mode;
|
||||
this->write_opcode_(RADIO_SET_STANDBY, &value, 1);
|
||||
}
|
||||
|
||||
uint32_t SX126xCommands::bandwidth_hz(SX126xBw bw) { return BW_HZ[bw]; }
|
||||
uint16_t SX126xCommands::ramp_us(uint8_t ramp) { return progmem_read_uint16(&RAMP[ramp]); }
|
||||
|
||||
void SX126xCommands::clear_irqs(uint16_t mask) {
|
||||
const uint8_t data[] = {static_cast<uint8_t>(mask >> 8), static_cast<uint8_t>(mask)};
|
||||
this->write_opcode_(RADIO_CLR_IRQSTATUS, data, 2);
|
||||
}
|
||||
|
||||
uint16_t SX126xCommands::read_irq() {
|
||||
uint8_t data[2]{};
|
||||
this->read_opcode_(RADIO_GET_IRQSTATUS, data, 2);
|
||||
return (uint16_t(data[0]) << 8) | data[1];
|
||||
}
|
||||
|
||||
void SX126xCommands::write_fifo(const uint8_t *data, size_t size) {
|
||||
const uint8_t header[] = {RADIO_WRITE_BUFFER, 0};
|
||||
this->spi_device_.write(header, {data, size});
|
||||
}
|
||||
|
||||
void SX126xCommands::read_packet(uint8_t modulation, std::vector<uint8_t> &packet, float &rssi, float &snr) {
|
||||
uint8_t data[3]{};
|
||||
this->read_opcode_(RADIO_GET_PACKETSTATUS, data, 3);
|
||||
rssi = (modulation == PACKET_TYPE_LORA ? data[0] : data[2]) / -2.0f;
|
||||
// FSK packets report no SNR
|
||||
snr = modulation == PACKET_TYPE_LORA ? static_cast<int8_t>(data[1]) / 4.0f : NAN;
|
||||
this->read_opcode_(RADIO_GET_RXBUFFERSTATUS, data, 2);
|
||||
if (!this->spi_device_.ok()) {
|
||||
return; // The length and offset were not read.
|
||||
}
|
||||
packet.resize(data[0]);
|
||||
const uint8_t header[] = {RADIO_READ_BUFFER, data[1], 0};
|
||||
this->spi_device_.read(header, packet);
|
||||
}
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
#include "radio_profile.h"
|
||||
#include "sx126x_spi_device.h"
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
enum class ConfigurationResult {
|
||||
CONFIGURATION_RESULT_OK,
|
||||
CONFIGURATION_RESULT_BUSY_TIMEOUT,
|
||||
CONFIGURATION_RESULT_UNSUPPORTED
|
||||
};
|
||||
|
||||
// Encodes chip commands. No ESPHome lifecycle, callbacks, or raw SPI access.
|
||||
// Commands record failure in the SPI device: callers start a sequence with begin()
|
||||
// and check ok() at its end, or before acting on a value that was read.
|
||||
class SX126xCommands {
|
||||
public:
|
||||
SX126xCommands() = default;
|
||||
SX126xCommands(const SX126xCommands &) = delete;
|
||||
SX126xCommands &operator=(const SX126xCommands &) = delete;
|
||||
SX126xSpiDevice &bus() { return this->spi_device_; }
|
||||
// Fills applied as it goes; only use it if the result is OK.
|
||||
[[nodiscard]] ConfigurationResult configure(const RadioProfile &profile, std::span<char, 16> version,
|
||||
AppliedProfile &applied);
|
||||
// Sets the payload length in the applied packet parameters and writes them.
|
||||
void set_packet_length(AppliedProfile &applied, uint8_t length);
|
||||
void calibrate_image(uint32_t frequency);
|
||||
void standby(SX126xStandbyMode mode);
|
||||
void receive();
|
||||
void transmit(uint32_t timeout_ms = 0);
|
||||
void sleep(bool cold = false);
|
||||
void clear_irqs(uint16_t mask = 0xFFFF);
|
||||
// Returns 0 if the read was skipped or failed.
|
||||
uint16_t read_irq();
|
||||
void write_fifo(const uint8_t *data, size_t size);
|
||||
// Leaves the packet unchanged if a read fails.
|
||||
void read_packet(uint8_t modulation, std::vector<uint8_t> &packet, float &rssi, float &snr);
|
||||
static uint32_t bandwidth_hz(SX126xBw bw);
|
||||
static uint16_t ramp_us(uint8_t ramp);
|
||||
|
||||
private:
|
||||
void read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
|
||||
void write_opcode_(uint8_t opcode, const uint8_t *data, uint8_t size);
|
||||
void read_register_(uint16_t reg, uint8_t *data, uint8_t size);
|
||||
void write_register_(uint16_t reg, const uint8_t *data, uint8_t size);
|
||||
static void encode_packet_params(const RadioProfile &profile, AppliedProfile &applied);
|
||||
SX126xSpiDevice spi_device_;
|
||||
};
|
||||
} // namespace esphome::sx126x
|
||||
@@ -0,0 +1,253 @@
|
||||
#include "sx126x_radio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <utility>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
// No ESP_LOG calls in this file: log listeners are user code, see sx126x_radio.h.
|
||||
|
||||
static constexpr uint32_t RESET_DELAY_HIGH_US = 5000;
|
||||
static constexpr uint32_t RESET_DELAY_LOW_US = 2000;
|
||||
static constexpr uint32_t RECEIVE_RETRY_INTERVAL_MS = 200;
|
||||
|
||||
RadioEffects SX126xRadio::comm_report(bool ok) {
|
||||
RadioEffects effects;
|
||||
effects.report = true;
|
||||
effects.warning = ok ? nullptr : LOG_STR("Wait busy timeout");
|
||||
return effects;
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::configure(const RadioProfile &requested, std::span<char, 16> version) {
|
||||
ESPHOME_DEBUG_ASSERT(this->can_configure());
|
||||
this->busy_ = true;
|
||||
this->rx_retry_pending_ = false;
|
||||
this->pending_irq_ = 0;
|
||||
RadioEffects effects;
|
||||
effects.report = true;
|
||||
effects.warning = this->apply_configuration_(requested, version);
|
||||
this->commit_(effects.warning == nullptr ? RadioState::RADIO_STATE_IDLE
|
||||
: RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
|
||||
return effects;
|
||||
}
|
||||
|
||||
const LogString *SX126xRadio::apply_configuration_(const RadioProfile &requested, std::span<char, 16> version) {
|
||||
if (!requested.sync_valid) {
|
||||
return LOG_STR("Invalid sync value");
|
||||
}
|
||||
this->rst_pin_->digital_write(true);
|
||||
delayMicroseconds(RESET_DELAY_HIGH_US);
|
||||
this->rst_pin_->digital_write(false);
|
||||
delayMicroseconds(RESET_DELAY_LOW_US);
|
||||
this->rst_pin_->digital_write(true);
|
||||
delayMicroseconds(RESET_DELAY_HIGH_US);
|
||||
this->bus().begin();
|
||||
AppliedProfile applied;
|
||||
switch (this->commands_.configure(requested, version, applied)) {
|
||||
case ConfigurationResult::CONFIGURATION_RESULT_OK:
|
||||
this->applied_ = applied;
|
||||
return nullptr;
|
||||
case ConfigurationResult::CONFIGURATION_RESULT_UNSUPPORTED:
|
||||
return LOG_STR("Unsupported chip version");
|
||||
default:
|
||||
return LOG_STR("Wait busy timeout");
|
||||
}
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::change_mode(ModeCommand command, uint8_t arg) {
|
||||
ESPHOME_DEBUG_ASSERT(this->can_change_mode());
|
||||
this->busy_ = true;
|
||||
this->bus().begin();
|
||||
switch (command) {
|
||||
case ModeCommand::MODE_COMMAND_RX:
|
||||
this->commands_.receive();
|
||||
break;
|
||||
case ModeCommand::MODE_COMMAND_TX:
|
||||
this->commands_.transmit();
|
||||
break;
|
||||
case ModeCommand::MODE_COMMAND_STANDBY:
|
||||
this->commands_.standby(static_cast<SX126xStandbyMode>(arg));
|
||||
break;
|
||||
case ModeCommand::MODE_COMMAND_SLEEP:
|
||||
case ModeCommand::MODE_COMMAND_COLD_SLEEP:
|
||||
this->commands_.sleep(command == ModeCommand::MODE_COMMAND_COLD_SLEEP);
|
||||
break;
|
||||
case ModeCommand::MODE_COMMAND_IMAGE_CAL:
|
||||
this->commands_.calibrate_image(this->applied_.frequency);
|
||||
break;
|
||||
}
|
||||
const bool ok = this->bus().ok();
|
||||
// Cold sleep loses the configuration, and a failed command leaves the chip state unknown.
|
||||
this->commit_(ok && command != ModeCommand::MODE_COMMAND_COLD_SLEEP ? RadioState::RADIO_STATE_IDLE
|
||||
: RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
|
||||
return comm_report(ok);
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::start_transmit(SX126xTransmitListener *listener, const uint8_t *data, size_t len,
|
||||
uint32_t timeout_ms) {
|
||||
RadioEffects effects;
|
||||
if (this->state_ == RadioState::RADIO_STATE_NEEDS_CONFIGURATION) {
|
||||
effects.result = SX126xError::SX126X_ERROR_NOT_READY;
|
||||
return effects;
|
||||
}
|
||||
if (this->busy_ || this->state_ != RadioState::RADIO_STATE_IDLE) {
|
||||
effects.result = SX126xError::SX126X_ERROR_BUSY;
|
||||
return effects;
|
||||
}
|
||||
const uint8_t fixed_length = this->applied_.payload_length;
|
||||
if (data == nullptr || len == 0 || len > this->max_packet_size() || (fixed_length != 0 && len != fixed_length)) {
|
||||
effects.result = SX126xError::SX126X_ERROR_INVALID_PARAMS;
|
||||
return effects;
|
||||
}
|
||||
this->busy_ = true;
|
||||
this->bus().begin();
|
||||
this->commands_.standby(STDBY_XOSC);
|
||||
// RX cannot advance in standby. Keep a packet that completed before standby intact for
|
||||
// poll(), before clearing IRQs or overwriting the shared FIFO. A failed read returns 0.
|
||||
// A packet that failed its CRC is not delivered, so it does not hold up the send.
|
||||
const uint16_t irq = this->commands_.read_irq() | this->pending_irq_;
|
||||
if ((irq & IRQ_RX_DONE) && !(irq & IRQ_CRC_ERROR)) {
|
||||
this->commit_(RadioState::RADIO_STATE_HOLDING_RX);
|
||||
effects.result = SX126xError::SX126X_ERROR_BUSY;
|
||||
return effects;
|
||||
}
|
||||
this->commands_.clear_irqs();
|
||||
this->pending_irq_ = 0;
|
||||
if (fixed_length == 0) {
|
||||
this->commands_.set_packet_length(this->applied_, len);
|
||||
}
|
||||
this->commands_.write_fifo(data, len);
|
||||
this->commands_.transmit(timeout_ms);
|
||||
if (!this->bus().ok()) {
|
||||
const bool restored = this->restore_();
|
||||
this->commit_(restored ? RadioState::RADIO_STATE_IDLE : RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
|
||||
effects = comm_report(restored);
|
||||
effects.result = SX126xError::SX126X_ERROR_BUSY_TIMEOUT;
|
||||
return effects;
|
||||
}
|
||||
this->tx_ = {listener, millis(), timeout_ms};
|
||||
this->commit_(RadioState::RADIO_STATE_TRANSMITTING);
|
||||
// No status report on success, so no user code runs before the caller has the result.
|
||||
// The completion reports status instead.
|
||||
return effects;
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::poll_transmit(uint32_t now) {
|
||||
if (this->busy_ || !this->is_transmitting()) {
|
||||
return {};
|
||||
}
|
||||
bool done = false;
|
||||
SX126xError outcome = SX126xError::SX126X_ERROR_NONE;
|
||||
if (this->dio1_pin_->digital_read()) {
|
||||
this->busy_ = true;
|
||||
this->bus().begin();
|
||||
const uint16_t status = this->commands_.read_irq();
|
||||
this->commands_.clear_irqs(status);
|
||||
this->busy_ = false;
|
||||
if (!this->bus().ok()) {
|
||||
outcome = SX126xError::SX126X_ERROR_BUSY_TIMEOUT;
|
||||
done = true;
|
||||
} else if (status & IRQ_RX_TX_TIMEOUT) {
|
||||
outcome = SX126xError::SX126X_ERROR_TIMEOUT;
|
||||
done = true;
|
||||
} else if (status & IRQ_TX_DONE) {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
// A cached loop tick can precede admission; signed subtraction also handles wraparound.
|
||||
if (!done && static_cast<int32_t>(now - this->tx_.started_ms) >= static_cast<int32_t>(this->tx_.timeout_ms)) {
|
||||
outcome = SX126xError::SX126X_ERROR_TIMEOUT;
|
||||
done = true;
|
||||
}
|
||||
return done ? this->finish_transmit_(outcome) : RadioEffects{};
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::cancel_transmit(SX126xTransmitListener *listener) {
|
||||
if (this->busy_ || !this->is_transmitting() || listener == nullptr || listener != this->tx_.listener) {
|
||||
return {};
|
||||
}
|
||||
return this->finish_transmit_(SX126xError::SX126X_ERROR_CANCELLED);
|
||||
}
|
||||
|
||||
// The only place a send ends.
|
||||
RadioEffects SX126xRadio::finish_transmit_(SX126xError result) {
|
||||
this->busy_ = true;
|
||||
const bool restored = this->restore_();
|
||||
this->commit_(restored ? RadioState::RADIO_STATE_IDLE : RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
|
||||
RadioEffects effects = comm_report(restored);
|
||||
effects.result = restored ? result : SX126xError::SX126X_ERROR_BUSY_TIMEOUT;
|
||||
effects.listener = std::exchange(this->tx_.listener, nullptr);
|
||||
effects.finished = true;
|
||||
return effects;
|
||||
}
|
||||
|
||||
bool SX126xRadio::restore_() {
|
||||
this->bus().begin();
|
||||
this->commands_.standby(STDBY_XOSC);
|
||||
this->commands_.clear_irqs();
|
||||
if (this->applied_.payload_length == 0) {
|
||||
this->commands_.set_packet_length(this->applied_, this->max_packet_size());
|
||||
}
|
||||
if (this->applied_.rx_start) {
|
||||
this->commands_.receive();
|
||||
} else {
|
||||
this->commands_.sleep();
|
||||
}
|
||||
return this->bus().ok();
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::poll(uint32_t now, std::vector<uint8_t> &packet) {
|
||||
if (this->busy_) {
|
||||
return {};
|
||||
}
|
||||
if (this->is_transmitting()) {
|
||||
return this->poll_transmit(now);
|
||||
}
|
||||
RadioEffects effects;
|
||||
if (this->state_ == RadioState::RADIO_STATE_NEEDS_CONFIGURATION) {
|
||||
effects.sleep_loop = true; // Polling cannot recover the radio; configure() can.
|
||||
return effects;
|
||||
}
|
||||
if (this->rx_retry_pending_ && now - this->rx_retry_started_ms_ < RECEIVE_RETRY_INTERVAL_MS) {
|
||||
return effects;
|
||||
}
|
||||
const bool holding = this->state_ == RadioState::RADIO_STATE_HOLDING_RX;
|
||||
if (!holding && this->pending_irq_ == 0 && !this->dio1_pin_->digital_read()) {
|
||||
effects.sleep_loop = this->dio1_pin_->is_internal(); // Its interrupt wakes the loop.
|
||||
return effects;
|
||||
}
|
||||
return this->receive_(holding, now, packet);
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::receive_(bool holding, uint32_t now, std::vector<uint8_t> &packet) {
|
||||
this->busy_ = true;
|
||||
this->bus().begin();
|
||||
// A packet whose read failed has had its flags cleared, so they are kept until it is read.
|
||||
const uint16_t status = this->commands_.read_irq() | this->pending_irq_;
|
||||
const bool received = (status & IRQ_RX_DONE) && !(status & IRQ_CRC_ERROR);
|
||||
if (!holding) {
|
||||
// Cleared before the read, so a packet that completes during the read raises DIO1 again.
|
||||
// A held packet keeps its IRQ until restoration.
|
||||
this->commands_.clear_irqs(status);
|
||||
}
|
||||
float rssi = 0, snr = 0;
|
||||
if (received) {
|
||||
this->commands_.read_packet(this->applied_.modulation, packet, rssi, snr);
|
||||
}
|
||||
const bool read = this->bus().ok();
|
||||
this->pending_irq_ = read ? 0 : status;
|
||||
// Do not spend a BUSY timeout on every loop while a read keeps failing.
|
||||
this->rx_retry_pending_ = !read;
|
||||
this->rx_retry_started_ms_ = now;
|
||||
RadioState next = this->state_;
|
||||
if (read && holding) {
|
||||
next = this->restore_() ? RadioState::RADIO_STATE_IDLE : RadioState::RADIO_STATE_NEEDS_CONFIGURATION;
|
||||
}
|
||||
this->commit_(next);
|
||||
RadioEffects effects = comm_report(read && next != RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
|
||||
effects.received = read && received;
|
||||
effects.rssi = rssi;
|
||||
effects.snr = snr;
|
||||
return effects;
|
||||
}
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -0,0 +1,149 @@
|
||||
#pragma once
|
||||
|
||||
#include "radio_profile.h"
|
||||
#include "sx126x_commands.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
enum class SX126xError {
|
||||
SX126X_ERROR_NONE = 0,
|
||||
SX126X_ERROR_TIMEOUT,
|
||||
SX126X_ERROR_INVALID_PARAMS,
|
||||
SX126X_ERROR_NOT_READY,
|
||||
SX126X_ERROR_BUSY_TIMEOUT,
|
||||
SX126X_ERROR_BUSY,
|
||||
SX126X_ERROR_CANCELLED
|
||||
};
|
||||
|
||||
class SX126xTransmitListener {
|
||||
public:
|
||||
virtual void on_transmit_complete(SX126xError result) = 0;
|
||||
};
|
||||
|
||||
// Who may use the radio between operations. This is driver state, not the chip's mode.
|
||||
enum class RadioState : uint8_t {
|
||||
// Startup, cold sleep, or a failed configuration, mode command or restoration. Only
|
||||
// configure() is accepted; sends return NOT_READY and the loop sleeps. SX126x configures
|
||||
// again every second after a failure, and on the next mode command after cold sleep.
|
||||
RADIO_STATE_NEEDS_CONFIGURATION,
|
||||
// Available. The chip is receiving or sleeping, as configured, or after the TX mode command
|
||||
// sending whatever the FIFO holds. That send is not tracked: a send or mode command may cut
|
||||
// it short, and poll() clears its TX_DONE as a flag without a packet.
|
||||
RADIO_STATE_IDLE,
|
||||
// A send is on air. SPI is free between polls; only polling and cancellation are accepted.
|
||||
RADIO_STATE_TRANSMITTING,
|
||||
// Standby with a received packet left in the FIFO, found when a send entered standby.
|
||||
// The send was rejected with BUSY; the next poll() delivers the packet and restores the
|
||||
// radio. configure() may discard it to recover from repeated read failures.
|
||||
RADIO_STATE_HOLDING_RX,
|
||||
};
|
||||
|
||||
enum class ModeCommand : uint8_t {
|
||||
MODE_COMMAND_RX,
|
||||
MODE_COMMAND_TX,
|
||||
MODE_COMMAND_STANDBY,
|
||||
MODE_COMMAND_SLEEP,
|
||||
MODE_COMMAND_COLD_SLEEP,
|
||||
MODE_COMMAND_IMAGE_CAL,
|
||||
};
|
||||
|
||||
// What a radio operation leaves for its caller to do. The radio has already committed its
|
||||
// state; the caller reports status first, then calls the listener or delivers the packet.
|
||||
struct RadioEffects {
|
||||
SX126xError result{SX126xError::SX126X_ERROR_NONE};
|
||||
// The completion to notify when finished is set; null for a blocking send.
|
||||
SX126xTransmitListener *listener{nullptr};
|
||||
// With report set: the warning to set, or null to clear it.
|
||||
const LogString *warning{nullptr};
|
||||
float rssi{0};
|
||||
float snr{0};
|
||||
bool report{false};
|
||||
// A send ended, and result is its outcome.
|
||||
bool finished{false};
|
||||
// The packet buffer holds a packet to deliver.
|
||||
bool received{false};
|
||||
// Nothing to poll until the next DIO1 edge or configuration.
|
||||
bool sleep_loop{false};
|
||||
};
|
||||
|
||||
// Owns the radio state and every SPI command sequence.
|
||||
//
|
||||
// Status reports, completions and packet listeners can run user code, which may call back
|
||||
// into the driver. Every operation therefore does its SPI work with busy() set, commits the
|
||||
// next state, and returns RadioEffects; SX126x::apply_() then reports status and calls the
|
||||
// listeners. This class has no access to component status or listeners, so callbacks only
|
||||
// ever see a committed state. Do not log here either: log listeners are user code. busy()
|
||||
// covers re-entry during a sequence, for example from a GPIO pin that logs.
|
||||
//
|
||||
// Each sequence starts with bus().begin(); after a BUSY timeout the rest is skipped and
|
||||
// bus().ok() is false. A failed send preparation restores the radio in a new sequence. A
|
||||
// failed restoration needs configuration, and its result overrides the send result. A
|
||||
// successful send start reports nothing, so no user code runs before the caller has the
|
||||
// result. Every send ends in finish_transmit_(), exactly once.
|
||||
//
|
||||
// A failed receive read keeps the packet and retries after RECEIVE_RETRY_INTERVAL_MS, so a
|
||||
// stuck BUSY line does not cost a 20 ms wait on every loop.
|
||||
class SX126xRadio {
|
||||
public:
|
||||
SX126xRadio() = default;
|
||||
SX126xRadio(const SX126xRadio &) = delete;
|
||||
SX126xRadio &operator=(const SX126xRadio &) = delete;
|
||||
SX126xSpiDevice &bus() { return this->commands_.bus(); }
|
||||
void set_dio1_pin(GPIOPin *pin) { this->dio1_pin_ = pin; }
|
||||
void set_rst_pin(GPIOPin *pin) { this->rst_pin_ = pin; }
|
||||
GPIOPin *get_dio1_pin() const { return this->dio1_pin_; }
|
||||
GPIOPin *get_rst_pin() const { return this->rst_pin_; }
|
||||
RadioState state() const { return this->state_; }
|
||||
bool busy() const { return this->busy_; }
|
||||
bool is_transmitting() const { return this->state_ == RadioState::RADIO_STATE_TRANSMITTING; }
|
||||
// A held packet may be discarded: explicit reconfiguration is the recovery path.
|
||||
bool can_configure() const { return !this->busy_ && !this->is_transmitting(); }
|
||||
bool can_change_mode() const { return !this->busy_ && this->state_ == RadioState::RADIO_STATE_IDLE; }
|
||||
size_t max_packet_size() const { return this->applied_.max_packet_size(); }
|
||||
|
||||
// Requires can_configure(). Resets the chip and applies the requested profile.
|
||||
RadioEffects configure(const RadioProfile &requested, std::span<char, 16> version);
|
||||
// Requires can_change_mode(). Cold sleep and failed commands need configuration afterwards.
|
||||
RadioEffects change_mode(ModeCommand command, uint8_t arg);
|
||||
// Result SX126X_ERROR_NONE starts a send that finishes exactly once, through poll_transmit() or
|
||||
// cancel_transmit(). Any other result owes no completion.
|
||||
RadioEffects start_transmit(SX126xTransmitListener *listener, const uint8_t *data, size_t len, uint32_t timeout_ms);
|
||||
// Sets finished only when it ended the current send.
|
||||
RadioEffects poll_transmit(uint32_t now);
|
||||
// Sets finished only if listener owns the current send.
|
||||
RadioEffects cancel_transmit(SX126xTransmitListener *listener);
|
||||
// One loop step: polls a send, or reads a received packet into packet.
|
||||
RadioEffects poll(uint32_t now, std::vector<uint8_t> &packet);
|
||||
|
||||
private:
|
||||
struct TxOperation {
|
||||
SX126xTransmitListener *listener{nullptr};
|
||||
uint32_t started_ms{0};
|
||||
uint32_t timeout_ms{0};
|
||||
};
|
||||
void commit_(RadioState state) {
|
||||
this->state_ = state;
|
||||
this->busy_ = false;
|
||||
}
|
||||
static RadioEffects comm_report(bool ok);
|
||||
const LogString *apply_configuration_(const RadioProfile &requested, std::span<char, 16> version);
|
||||
bool restore_();
|
||||
RadioEffects finish_transmit_(SX126xError result);
|
||||
RadioEffects receive_(bool holding, uint32_t now, std::vector<uint8_t> &packet);
|
||||
SX126xCommands commands_;
|
||||
AppliedProfile applied_;
|
||||
TxOperation tx_;
|
||||
uint32_t rx_retry_started_ms_{0};
|
||||
uint16_t pending_irq_{0}; // flags of a received packet whose read failed after they were cleared
|
||||
GPIOPin *dio1_pin_{nullptr};
|
||||
GPIOPin *rst_pin_{nullptr};
|
||||
RadioState state_{RadioState::RADIO_STATE_NEEDS_CONFIGURATION};
|
||||
bool busy_{false};
|
||||
bool rx_retry_pending_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "sx126x_spi_device.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
static constexpr uint32_t BUSY_TIMEOUT_MS = 20;
|
||||
static constexpr uint32_t SWITCHING_DELAY_US = 1;
|
||||
|
||||
void SX126xSpiDevice::exchange_(std::span<const uint8_t> header, std::span<const uint8_t> tx, std::span<uint8_t> rx) {
|
||||
if (!this->ok_) {
|
||||
return;
|
||||
}
|
||||
// Selecting first wakes a sleeping radio. The guard releases CS even on timeout.
|
||||
Selection selected(this->device_);
|
||||
const uint32_t start = millis();
|
||||
while (this->busy_pin_->digital_read()) {
|
||||
if (millis() - start > BUSY_TIMEOUT_MS) {
|
||||
this->ok_ = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (auto byte : header) {
|
||||
this->device_.transfer_byte(byte);
|
||||
}
|
||||
for (auto byte : tx) {
|
||||
this->device_.transfer_byte(byte);
|
||||
}
|
||||
for (auto &byte : rx) {
|
||||
byte = this->device_.transfer_byte(0);
|
||||
}
|
||||
}
|
||||
|
||||
void SX126xSpiDevice::write(std::span<const uint8_t> header, std::span<const uint8_t> data) {
|
||||
this->exchange_(header, data, {});
|
||||
if (this->ok_) {
|
||||
// This delay belongs after CS is released, not in the selected interval.
|
||||
delayMicroseconds(SWITCHING_DELAY_US);
|
||||
}
|
||||
}
|
||||
|
||||
void SX126xSpiDevice::read(std::span<const uint8_t> header, std::span<uint8_t> data) {
|
||||
this->exchange_(header, {}, data);
|
||||
}
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include <span>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
// Persistent radio device. read()/write() use exchange_() for one complete selected interval.
|
||||
// No raw SPI access escapes. Call only from the main loop; spans are borrowed until return.
|
||||
// Failures stick: after a BUSY timeout every exchange is skipped until the next begin(),
|
||||
// so a command sequence checks ok() once instead of after every command.
|
||||
class SX126xSpiDevice {
|
||||
public:
|
||||
SX126xSpiDevice() = default;
|
||||
SX126xSpiDevice(const SX126xSpiDevice &) = delete;
|
||||
SX126xSpiDevice &operator=(const SX126xSpiDevice &) = delete;
|
||||
void set_spi_parent(spi::SPIComponent *parent) { this->device_.set_spi_parent(parent); }
|
||||
void set_cs_pin(GPIOPin *pin) { this->device_.set_cs_pin(pin); }
|
||||
void set_data_rate(uint32_t rate) { this->device_.set_data_rate(rate); }
|
||||
void set_mode(spi::SPIMode mode) { this->device_.set_mode(mode); }
|
||||
void set_bit_order(spi::SPIBitOrder order) { this->device_.set_bit_order(order); }
|
||||
void set_release_device(bool release) { this->device_.set_release_device(release); }
|
||||
void set_write_only(bool write_only) { this->device_.set_write_only(write_only); }
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
void set_psram_dma(bool enable) { this->device_.set_psram_dma(enable); }
|
||||
#endif
|
||||
|
||||
void set_busy_pin(GPIOPin *pin) { this->busy_pin_ = pin; }
|
||||
GPIOPin *get_busy_pin() const { return this->busy_pin_; }
|
||||
GPIOPin *get_cs_pin() const { return this->device_.get_cs_pin(); }
|
||||
void setup() { this->device_.spi_setup(); }
|
||||
void teardown() { this->device_.spi_teardown(); }
|
||||
// Starts a command sequence with no recorded failure.
|
||||
void begin() { this->ok_ = true; }
|
||||
// False once any exchange since begin() timed out waiting for BUSY.
|
||||
bool ok() const { return this->ok_; }
|
||||
void write(std::span<const uint8_t> header, std::span<const uint8_t> data);
|
||||
// Leaves data unchanged when the exchange is skipped or times out.
|
||||
void read(std::span<const uint8_t> header, std::span<uint8_t> data);
|
||||
|
||||
private:
|
||||
class Device final : public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_8MHZ> {
|
||||
public:
|
||||
GPIOPin *get_cs_pin() const { return this->cs_; }
|
||||
};
|
||||
class Selection {
|
||||
public:
|
||||
explicit Selection(Device &device) : device_(device) { this->device_.enable(); }
|
||||
~Selection() { this->device_.disable(); }
|
||||
Selection(const Selection &) = delete;
|
||||
Selection &operator=(const Selection &) = delete;
|
||||
|
||||
private:
|
||||
Device &device_;
|
||||
};
|
||||
void exchange_(std::span<const uint8_t> header, std::span<const uint8_t> tx, std::span<uint8_t> rx);
|
||||
Device device_;
|
||||
GPIOPin *busy_pin_{nullptr};
|
||||
bool ok_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -5,13 +5,15 @@ from esphome.const import CONF_MODEL
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
CODEOWNERS = ["@glmnet", "@arpena"]
|
||||
CODEOWNERS = ["@glmnet", "@arpena", "@Rodrigoah"]
|
||||
|
||||
whirlpool_ns = cg.esphome_ns.namespace("whirlpool")
|
||||
WhirlpoolClimate = whirlpool_ns.class_("WhirlpoolClimate", climate_ir.ClimateIR)
|
||||
|
||||
Model = whirlpool_ns.enum("Model")
|
||||
MODELS = Model.options("DG11J1-3A", "DG11J1-91", "HISENSE_ARG", prefix="MODEL_")
|
||||
MODELS = Model.options(
|
||||
"DG11J1-3A", "DG11J1-91", "HISENSE_ARG", "HITACHI_HCRA31NEWH", prefix="MODEL_"
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(WhirlpoolClimate).extend(
|
||||
{
|
||||
@@ -26,3 +28,5 @@ async def to_code(config: ConfigType) -> None:
|
||||
# cv.enum keeps the YAML key as a str subclass; the MockObj value cannot be compared
|
||||
if config[CONF_MODEL] == "HISENSE_ARG":
|
||||
cg.add_define("USE_WHIRLPOOL_HISENSE_ARG")
|
||||
elif config[CONF_MODEL] == "HITACHI_HCRA31NEWH":
|
||||
cg.add_define("USE_WHIRLPOOL_HITACHI_HCRA31NEWH")
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::whirlpool {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "whirlpool.climate");
|
||||
@@ -44,6 +46,17 @@ static constexpr uint8_t HISENSE_ARG_POWER_TOGGLE = 0x01; // byte 15
|
||||
static constexpr uint8_t HISENSE_ARG_SETTINGS_ONLY = 0x02; // byte 15
|
||||
static constexpr uint8_t HISENSE_ARG_FIXED_TEMP_STEP = 7; // auto and fan-only send 23 C
|
||||
|
||||
// Hitachi HCRA31NEWH model: power is a state in every frame
|
||||
static constexpr uint8_t HITACHI_BYTE10 = 0x20; // byte 10
|
||||
static constexpr uint8_t HITACHI_COOL = 0x02; // byte 15
|
||||
static constexpr uint8_t HITACHI_OFF = 0x01; // byte 15
|
||||
static constexpr uint8_t HITACHI_BYTE18_ON = 0x38; // byte 18
|
||||
static constexpr uint8_t HITACHI_BYTE18_OFF = 0x28; // byte 18
|
||||
static constexpr uint8_t HITACHI_BYTE19 = 0x08; // byte 19
|
||||
|
||||
// Header, 21 bytes of 8 bits, two dividers and the footer mark
|
||||
static constexpr uint32_t WHIRLPOOL_FRAME_TIMINGS = 2 + WHIRLPOOL_STATE_LENGTH * 16 + 2 * 2 + 1;
|
||||
|
||||
WhirlpoolClimate::WhirlpoolClimate()
|
||||
: climate_ir::ClimateIR(
|
||||
WHIRLPOOL_DG11J1_3A_TEMP_MIN, WHIRLPOOL_DG11J1_3A_TEMP_MAX, 1.0f, true, true,
|
||||
@@ -55,19 +68,19 @@ void WhirlpoolClimate::transmit_state() {
|
||||
this->last_transmit_time_ = millis(); // setting the time of the last transmission.
|
||||
uint8_t remote_state[WHIRLPOOL_STATE_LENGTH];
|
||||
memset(remote_state, 0, sizeof(remote_state));
|
||||
remote_state[0] = 0x83;
|
||||
remote_state[1] = 0x06;
|
||||
put_be16(remote_state, this->remote_id_());
|
||||
remote_state[6] = 0x80;
|
||||
remote_state[18] = 0x08; // the same on every supported model
|
||||
remote_state[18] = 0x08;
|
||||
|
||||
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
|
||||
const bool power_toggle = powered_on != this->powered_on_assumed;
|
||||
const bool powered_on = this->mode != climate::CLIMATE_MODE_OFF;
|
||||
// Hitachi sends power as a state instead of a toggle
|
||||
const bool power_toggle = !this->is_hitachi_hcra31newh_() && powered_on != this->powered_on_assumed;
|
||||
if (power_toggle) {
|
||||
// Set power toggle command
|
||||
remote_state[2] = 4;
|
||||
remote_state[15] = 1;
|
||||
this->powered_on_assumed = powered_on;
|
||||
}
|
||||
this->powered_on_assumed = powered_on;
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
// set fan auto
|
||||
@@ -93,6 +106,10 @@ void WhirlpoolClimate::transmit_state() {
|
||||
remote_state[15] = 6;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
// These remotes keep the cool code in the power-off frame
|
||||
if (this->is_hisense_arg_() || this->is_hitachi_hcra31newh_())
|
||||
remote_state[3] = WHIRLPOOL_COOL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -102,6 +119,7 @@ void WhirlpoolClimate::transmit_state() {
|
||||
// Temperature
|
||||
auto temp = (uint8_t) roundf(clamp(this->target_temperature, this->temperature_min_(), this->temperature_max_()));
|
||||
const auto temp_step = (uint8_t) (temp - this->temperature_min_());
|
||||
this->target_temperature = temp; // publish what the unit was sent
|
||||
if (hisense_arg && this->fixed_temp_mode_()) {
|
||||
remote_state[3] |= HISENSE_ARG_FIXED_TEMP_STEP << 4;
|
||||
} else {
|
||||
@@ -131,7 +149,6 @@ void WhirlpoolClimate::transmit_state() {
|
||||
remote_state[2] |= WHIRLPOOL_SWING_MASK;
|
||||
remote_state[8] = WHIRLPOOL_SWING_BYTE8;
|
||||
}
|
||||
this->encode_hisense_arg_(remote_state, power_toggle, temp_step);
|
||||
} else if (this->send_swing_cmd_) {
|
||||
if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL || this->swing_mode == climate::CLIMATE_SWING_OFF) {
|
||||
remote_state[2] |= WHIRLPOOL_SWING_MASK;
|
||||
@@ -139,6 +156,11 @@ void WhirlpoolClimate::transmit_state() {
|
||||
}
|
||||
}
|
||||
|
||||
if (hisense_arg)
|
||||
this->encode_hisense_arg_(remote_state, power_toggle, temp_step);
|
||||
if (this->is_hitachi_hcra31newh_())
|
||||
this->encode_hitachi_hcra31newh_(remote_state);
|
||||
|
||||
// Checksum
|
||||
remote_state[13] = xor_bytes(&remote_state[2], 11, remote_state[13]);
|
||||
remote_state[20] = xor_bytes(&remote_state[14], 6, remote_state[20]);
|
||||
@@ -148,6 +170,7 @@ void WhirlpoolClimate::transmit_state() {
|
||||
// Send code
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
data->reserve(WHIRLPOOL_FRAME_TIMINGS);
|
||||
|
||||
data->set_carrier_frequency(38000);
|
||||
|
||||
@@ -180,10 +203,6 @@ void WhirlpoolClimate::encode_hisense_arg_(uint8_t *remote_state, bool power_tog
|
||||
remote_state[15] = power_toggle ? HISENSE_ARG_POWER_TOGGLE : HISENSE_ARG_SETTINGS_ONLY;
|
||||
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
// The remote keeps the cool code in the power-off frame
|
||||
remote_state[3] = (remote_state[3] & 0xF0) | WHIRLPOOL_COOL;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
// The unit runs these fan speeds itself; the user's choice is kept for the next mode change
|
||||
remote_state[2] &= ~WHIRLPOOL_FAN_MASK;
|
||||
@@ -212,6 +231,27 @@ void WhirlpoolClimate::encode_hisense_arg_(uint8_t *remote_state, bool power_tog
|
||||
#endif
|
||||
}
|
||||
|
||||
void WhirlpoolClimate::encode_hitachi_hcra31newh_(uint8_t *remote_state) const {
|
||||
#ifdef USE_WHIRLPOOL_HITACHI_HCRA31NEWH
|
||||
remote_state[10] = HITACHI_BYTE10;
|
||||
remote_state[18] = HITACHI_BYTE18_ON;
|
||||
remote_state[19] = HITACHI_BYTE19;
|
||||
// The unit takes the whole state from every frame: only the off frame carries the power bit
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
remote_state[15] = HITACHI_COOL;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
remote_state[2] |= WHIRLPOOL_POWER;
|
||||
remote_state[15] = HITACHI_OFF;
|
||||
remote_state[18] = HITACHI_BYTE18_OFF;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
// Check if the esp isn't currently transmitting.
|
||||
if (millis() - this->last_transmit_time_ < 500) {
|
||||
@@ -234,7 +274,8 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_GAP))
|
||||
return false;
|
||||
}
|
||||
if (i == WHIRLPOOL_SECOND_DIVIDER && !data.is_valid()) {
|
||||
// The Hitachi remote always sends all 21 bytes; byte 18 holds its power state
|
||||
if (i == WHIRLPOOL_SECOND_DIVIDER && !data.is_valid() && !this->is_hitachi_hcra31newh_()) {
|
||||
// Remote control only sent 14 bytes, nothing more to read, not even the footer
|
||||
ESP_LOGV(TAG, "Remote control only sent %d bytes", i);
|
||||
skip_footer = true;
|
||||
@@ -261,13 +302,18 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
ESP_LOGV_HEX(TAG, remote_state, WHIRLPOOL_STATE_LENGTH, WHIRLPOOL_STATE_LENGTH, "Received: %s");
|
||||
|
||||
// verify header remote code
|
||||
if (remote_state[0] != 0x83 || remote_state[1] != 0x06)
|
||||
if (encode_uint16(remote_state[0], remote_state[1]) != this->remote_id_())
|
||||
return false;
|
||||
|
||||
// powr on/off button
|
||||
ESP_LOGV(TAG, "Power: %02X", (remote_state[2] & WHIRLPOOL_POWER));
|
||||
|
||||
if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) {
|
||||
if (this->is_hitachi_hcra31newh_()) {
|
||||
// Power is a state: byte 18 says whether the unit is off
|
||||
this->powered_on_assumed = remote_state[18] != HITACHI_BYTE18_OFF;
|
||||
if (!this->powered_on_assumed)
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
} else if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) {
|
||||
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
|
||||
|
||||
if (powered_on) {
|
||||
@@ -309,7 +355,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
int temp = remote_state[3] & 0xF0;
|
||||
ESP_LOGVV(TAG, "Temperature Raw: %02X", temp);
|
||||
temp = (uint8_t) temp >> 4;
|
||||
temp += static_cast<int>(this->temperature_min_());
|
||||
temp = std::min(temp + static_cast<int>(this->temperature_min_()), static_cast<int>(this->temperature_max_()));
|
||||
ESP_LOGVV(TAG, "Temperature Climate: %u", temp);
|
||||
this->target_temperature = temp;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ namespace esphome::whirlpool {
|
||||
|
||||
/// Simple enum to represent models.
|
||||
enum Model {
|
||||
MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32
|
||||
MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30
|
||||
MODEL_HISENSE_ARG = 2, /// Hisense/BGH (Argentina), 16 to 30
|
||||
MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32
|
||||
MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30
|
||||
MODEL_HISENSE_ARG = 2, /// Hisense/BGH (Argentina), 16 to 30
|
||||
MODEL_HITACHI_HCRA31NEWH = 3, /// Hitachi with the HCRA31NEWH remote, 16 to 30
|
||||
};
|
||||
|
||||
// Temperature
|
||||
@@ -29,7 +30,9 @@ class WhirlpoolClimate final : public climate_ir::ClimateIR {
|
||||
|
||||
/// Override control to change settings of the climate device.
|
||||
void control(const climate::ClimateCall &call) override {
|
||||
send_swing_cmd_ = call.get_swing_mode().has_value();
|
||||
// The toggle remotes flip swing on every press, so only send one when the mode really changes
|
||||
auto swing = call.get_swing_mode();
|
||||
this->send_swing_cmd_ = swing.has_value() && *swing != this->swing_mode;
|
||||
climate_ir::ClimateIR::control(call);
|
||||
}
|
||||
|
||||
@@ -55,10 +58,22 @@ class WhirlpoolClimate final : public climate_ir::ClimateIR {
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
/// False when the model is compiled out, so the compiler drops the Hitachi branches.
|
||||
bool is_hitachi_hcra31newh_() const {
|
||||
#ifdef USE_WHIRLPOOL_HITACHI_HCRA31NEWH
|
||||
return this->model_ == MODEL_HITACHI_HCRA31NEWH;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
/// The remote ID sent in bytes 0 and 1.
|
||||
uint16_t remote_id_() const { return this->is_hitachi_hcra31newh_() ? 0xA959 : 0x8306; }
|
||||
/// Modes in which the Hisense ARG model sends a fixed temperature.
|
||||
bool fixed_temp_mode_() const;
|
||||
/// Add the Hisense ARG bytes on top of the common whirlpool frame.
|
||||
void encode_hisense_arg_(uint8_t *remote_state, bool power_toggle, uint8_t temp_step) const;
|
||||
/// Add the Hitachi HCRA31NEWH bytes on top of the common whirlpool frame.
|
||||
void encode_hitachi_hcra31newh_(uint8_t *remote_state) const;
|
||||
/// Set the time of the last transmission.
|
||||
uint32_t last_transmit_time_{};
|
||||
|
||||
|
||||
@@ -563,7 +563,7 @@ class WiFiComponent final : public Component {
|
||||
// (In most use cases you won't need these)
|
||||
/// Setup WiFi interface.
|
||||
void setup() override;
|
||||
#if defined(USE_LN882X) || defined(USE_BK72XX)
|
||||
#if defined(USE_ESP32) || defined(USE_LN882X) || defined(USE_BK72XX)
|
||||
void on_powerdown() override;
|
||||
#endif
|
||||
void start();
|
||||
|
||||
@@ -1635,6 +1635,21 @@ bool WiFiComponent::wifi_dpp_start_listen_() {
|
||||
void WiFiComponent::wifi_dpp_stop_listen_() { esp_supp_dpp_stop_listen(); }
|
||||
#endif
|
||||
|
||||
void WiFiComponent::on_powerdown() {
|
||||
// Leave the AP before esp_restart() or deep sleep stops Wi-Fi. esp_wifi_stop() while associated
|
||||
// makes the ESP32 retransmit its Disassociation up to the retry limit although the AP ACKs it
|
||||
// (32 copies with protected management frames), which some APs handle badly.
|
||||
// Upstream bug: https://github.com/espressif/esp-idf/issues/19443 - remove this once it is fixed
|
||||
// in the ESP-IDF versions we support.
|
||||
// Ask the driver: s_sta_connected lags until the main loop has handled the connect event.
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK)
|
||||
return;
|
||||
ESP_LOGD(TAG, "Disconnecting before powerdown");
|
||||
esp_wifi_disconnect();
|
||||
delay(100); // NOLINT: once per reboot or deep sleep, lets the disassociation go out
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
|
||||
|
||||
bssid_t WiFiComponent::wifi_bssid() {
|
||||
|
||||
@@ -294,6 +294,8 @@ void Application::reboot() {
|
||||
for (auto &component : std::ranges::reverse_view(this->components_)) {
|
||||
component->on_shutdown();
|
||||
}
|
||||
// No teardown on a forced reboot, but still power hardware down (Wi-Fi leaves the AP)
|
||||
this->run_powerdown_hooks();
|
||||
arch_restart();
|
||||
}
|
||||
void Application::safe_reboot() {
|
||||
|
||||
@@ -185,10 +185,12 @@ class Component {
|
||||
*/
|
||||
virtual bool teardown() { return true; }
|
||||
|
||||
/** Called after teardown is complete to power down hardware.
|
||||
/** Called last before a reboot or deep sleep to power down hardware.
|
||||
*
|
||||
* This is called after all components have finished their teardown process,
|
||||
* On a safe shutdown this is called after all components have finished their teardown process,
|
||||
* making it safe to power down hardware like ethernet PHY.
|
||||
* On a forced reboot (Application::reboot()) it is called right after on_shutdown() with no
|
||||
* teardown, so implementations must not assume teardown() has run.
|
||||
*/
|
||||
virtual void on_powerdown() {}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
#define USE_BUTTON
|
||||
#define USE_CAMERA
|
||||
#define USE_CANBUS_FD
|
||||
#define USE_CC1101_LONG_PACKET
|
||||
#define USE_CLIMATE
|
||||
#define USE_CLIMATE_VISUAL_OVERRIDES
|
||||
#define USE_CONTROLLER_REGISTRY
|
||||
@@ -387,6 +388,7 @@
|
||||
#define USE_WATER_HEATER
|
||||
#define USE_WATER_HEATER_VISUAL_OVERRIDES
|
||||
#define USE_WHIRLPOOL_HISENSE_ARG
|
||||
#define USE_WHIRLPOOL_HITACHI_HCRA31NEWH
|
||||
#define USE_ZWAVE_PROXY
|
||||
|
||||
// Feature flags which do not work for zephyr
|
||||
@@ -563,11 +565,6 @@
|
||||
#endif
|
||||
#define USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
|
||||
#define USE_BLE_GATT_CLIENT
|
||||
#define USE_BLE_GATT_BACKEND_BLUEDROID
|
||||
#define USE_BLUEDROID_GATT_SERVICE_TABLE
|
||||
#define USE_BLE_CLIENT_GATT_NODES
|
||||
#define USE_BLE_CLIENT_LEGACY_ENGINE
|
||||
#define ESPHOME_BLE_CLIENT_MAX_NODES 1
|
||||
#define ESPHOME_BLE_GATT_CLIENT_COUNT 1
|
||||
#define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1
|
||||
#define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1
|
||||
@@ -838,10 +835,7 @@
|
||||
#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1
|
||||
#define USE_BLE_SCAN_RESPONSE_MERGER
|
||||
#define USE_BLE_GATT_CLIENT
|
||||
#define USE_BLE_GATT_BACKEND_RP2
|
||||
#define USE_BLE_CLIENT_GATT_NODES
|
||||
#define ESPHOME_BLE_GATT_CLIENT_COUNT 3
|
||||
#define ESPHOME_BLE_CLIENT_MAX_NODES 1
|
||||
#define USE_RP2040_VARIANT_RP2040
|
||||
#define USE_RP2_WATCHDOG_TIMEOUT 8000
|
||||
#define USE_SPI
|
||||
|
||||
@@ -25,6 +25,9 @@ CORE_BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "core"
|
||||
# allow benchmarks to compile on the host platform.
|
||||
STUBS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "stubs"
|
||||
|
||||
# Shared test doubles, reached by -I so only the benchmarks that include them pull in their headers
|
||||
HELPERS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "helpers"
|
||||
|
||||
BUILD_FLAGS = [
|
||||
"-Os", # match firmware optimization level (detects inlining regressions)
|
||||
"-g", # debug symbols for profiling
|
||||
@@ -32,6 +35,7 @@ BUILD_FLAGS = [
|
||||
"-fdata-sections", # required for dead-code stripping with -Os
|
||||
"-DUSE_BENCHMARK", # disable WarnIfComponentBlockingGuard in finish()
|
||||
f"-I{STUBS_DIR}", # stub headers for ESP32-only components
|
||||
f"-I{HELPERS_DIR}", # shared test doubles such as fake_uart.h
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/ld2450/ld2450.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "fake_uart.h"
|
||||
|
||||
namespace esphome::ld2450::benchmarks {
|
||||
|
||||
using esphome::benchmarks::FakeUART;
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
static constexpr size_t FRAME_SIZE = 30;
|
||||
|
||||
// Periodic frames with one moving target at two positions
|
||||
static constexpr uint8_t FRAME_A[FRAME_SIZE] = {0xAA, 0xFF, 0x03, 0x00, 0x0E, 0x03, 0xB1, 0x86, 0x10, 0x00,
|
||||
0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xCC};
|
||||
static constexpr uint8_t FRAME_B[FRAME_SIZE] = {0xAA, 0xFF, 0x03, 0x00, 0x2A, 0x03, 0xC8, 0x86, 0x10, 0x00,
|
||||
0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xCC};
|
||||
|
||||
// --- loop() parsing one periodic frame, no entities attached ---
|
||||
|
||||
static void LD2450_PeriodicFrame(benchmark::State &state) {
|
||||
FakeUART uart;
|
||||
LD2450Component radar;
|
||||
radar.set_uart_parent(&uart);
|
||||
radar.set_link_timeout(60000);
|
||||
// Bring the link up outside the timed loop
|
||||
uart.load(FRAME_A, FRAME_SIZE);
|
||||
radar.loop();
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uart.load(FRAME_A, FRAME_SIZE);
|
||||
radar.loop();
|
||||
}
|
||||
benchmark::DoNotOptimize(uart.available());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(LD2450_PeriodicFrame);
|
||||
|
||||
// --- loop() parsing a moving target and publishing its sensors ---
|
||||
|
||||
static void LD2450_PeriodicFrameSensors(benchmark::State &state) {
|
||||
FakeUART uart;
|
||||
LD2450Component radar;
|
||||
radar.set_uart_parent(&uart);
|
||||
radar.set_link_timeout(60000);
|
||||
sensor::Sensor x, y, speed, distance, angle, count;
|
||||
binary_sensor::BinarySensor presence;
|
||||
radar.set_move_x_sensor(0, &x);
|
||||
radar.set_move_y_sensor(0, &y);
|
||||
radar.set_move_speed_sensor(0, &speed);
|
||||
radar.set_move_distance_sensor(0, &distance);
|
||||
radar.set_move_angle_sensor(0, &angle);
|
||||
radar.set_target_count_sensor(&count);
|
||||
radar.set_target_binary_sensor(&presence);
|
||||
uart.load(FRAME_A, FRAME_SIZE);
|
||||
radar.loop();
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uart.load((i & 1) ? FRAME_B : FRAME_A, FRAME_SIZE);
|
||||
radar.loop();
|
||||
}
|
||||
benchmark::DoNotOptimize(x.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(LD2450_PeriodicFrameSensors);
|
||||
|
||||
} // namespace esphome::ld2450::benchmarks
|
||||
@@ -0,0 +1,2 @@
|
||||
sensor:
|
||||
binary_sensor:
|
||||
@@ -0,0 +1,67 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "fake_uart.h"
|
||||
|
||||
namespace esphome::modbus::benchmarks {
|
||||
|
||||
using esphome::benchmarks::FakeUART;
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
static constexpr uint8_t ADDRESS = 1;
|
||||
static constexpr uint8_t REGISTER_COUNT = 10;
|
||||
// Read 10 holding registers from address 0, as an energy meter poll does
|
||||
static constexpr uint8_t REQUEST_PDU[] = {0x03, 0x00, 0x00, 0x00, REGISTER_COUNT};
|
||||
// Address + function + byte count + 20 data bytes + CRC
|
||||
static constexpr size_t RESPONSE_SIZE = 3 + REGISTER_COUNT * 2 + 2;
|
||||
|
||||
class SumDevice : public ModbusClientDevice {
|
||||
public:
|
||||
using ModbusClientDevice::ModbusClientDevice;
|
||||
void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
ResponseStatus status) override {
|
||||
for (uint16_t value : registers)
|
||||
this->sum += value;
|
||||
}
|
||||
uint32_t sum{0};
|
||||
};
|
||||
|
||||
class NoDelayHub : public ModbusClientHub {
|
||||
protected:
|
||||
// No wait before a send, so every loop() can send the next poll
|
||||
int32_t tx_delay_remaining() override { return 0; }
|
||||
};
|
||||
|
||||
// --- One continuous poll cycle per loop(): receive and CRC check the response, decode it, requeue and resend ---
|
||||
|
||||
static void Modbus_ReadHoldingPollCycle(benchmark::State &state) {
|
||||
// Address 1, function 3, byte count 20, 10 registers; the CRC goes in the last two bytes
|
||||
uint8_t response[RESPONSE_SIZE] = {0x01, 0x03, 0x14, 0x09, 0x1F, 0x00, 0x00, 0x01, 0xF4, 0x00, 0x00, 0x13,
|
||||
0x88, 0x00, 0x32, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x64, 0x12, 0x34};
|
||||
put_le16(&response[RESPONSE_SIZE - 2], crc16(response, RESPONSE_SIZE - 2));
|
||||
|
||||
FakeUART uart;
|
||||
uart.set_baud_rate(9600);
|
||||
NoDelayHub hub;
|
||||
hub.set_uart_parent(&uart);
|
||||
hub.setup();
|
||||
SumDevice device(&hub, ADDRESS);
|
||||
hub.queue_pdu(ADDRESS, REQUEST_PDU, &device, {.continuous = true});
|
||||
// The first loop() sends the poll; every send rewinds the response, so the device answers each one
|
||||
hub.loop();
|
||||
uart.load(response, RESPONSE_SIZE);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
hub.loop();
|
||||
}
|
||||
benchmark::DoNotOptimize(device.sum);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Modbus_ReadHoldingPollCycle);
|
||||
|
||||
} // namespace esphome::modbus::benchmarks
|
||||
@@ -0,0 +1,57 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "fake_uart.h"
|
||||
|
||||
namespace esphome::uart::benchmarks {
|
||||
|
||||
using esphome::benchmarks::FakeUART;
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// One second of a GPS module's NMEA output
|
||||
static constexpr char NMEA_BURST[] = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n"
|
||||
"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n"
|
||||
"$GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\r\n"
|
||||
"$GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\r\n"
|
||||
"$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48\r\n";
|
||||
|
||||
// --- read_available(): drain a whole NMEA burst through a byte-sum callback ---
|
||||
|
||||
static void UART_ReadAvailable_NmeaBurst(benchmark::State &state) {
|
||||
FakeUART uart;
|
||||
UARTDevice dev(&uart);
|
||||
uint32_t sum = 0;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uart.load(reinterpret_cast<const uint8_t *>(NMEA_BURST), sizeof(NMEA_BURST) - 1);
|
||||
dev.read_available([&](uint8_t b) { sum += b; });
|
||||
}
|
||||
benchmark::DoNotOptimize(sum);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(UART_ReadAvailable_NmeaBurst);
|
||||
|
||||
// --- discard_available(): drop a 256 byte backlog ---
|
||||
|
||||
// Two virtual calls per byte, available() and read_byte(): this measures that per-byte call overhead
|
||||
static void UART_DiscardAvailable(benchmark::State &state) {
|
||||
uint8_t backlog[256]{};
|
||||
FakeUART uart;
|
||||
UARTDevice dev(&uart);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uart.load(backlog, sizeof(backlog));
|
||||
dev.discard_available();
|
||||
}
|
||||
benchmark::DoNotOptimize(uart.available());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(UART_DiscardAvailable);
|
||||
|
||||
} // namespace esphome::uart::benchmarks
|
||||
@@ -0,0 +1,93 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/core/frame_reader.h"
|
||||
#include "esphome/core/line_reader.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
static constexpr char NMEA_GGA[] = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
|
||||
|
||||
// A 30 byte frame with a 4 byte header and a 2 byte tail
|
||||
static constexpr uint8_t FRAME_30[30] = {0xAA, 0xFF, 0x03, 0x00, 0x0E, 0x03, 0xB1, 0x86, 0x10, 0x00,
|
||||
0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xCC};
|
||||
|
||||
// Four A02YYUW distance frames among noise bytes; the doubled 0xFF fails the checksum and resyncs onto the
|
||||
// frame behind it. Ends on a whole frame, so every pass starts clean
|
||||
static constexpr uint8_t ULTRASONIC_NOISY[] = {0x12, 0xFF, 0x07, 0xA1, 0xA7, 0x34, 0xFF, 0xFF, 0x07, 0xA1, 0xA7,
|
||||
0x00, 0xFF, 0x07, 0xA1, 0xA7, 0x56, 0xFF, 0x07, 0xA1, 0xA7};
|
||||
|
||||
// --- LineReader: one GGA sentence fed byte by byte ---
|
||||
|
||||
static void LineReader_NmeaLine(benchmark::State &state) {
|
||||
LineReader<96> reader;
|
||||
const auto *bytes = reinterpret_cast<const uint8_t *>(NMEA_GGA);
|
||||
size_t total = 0;
|
||||
|
||||
for (auto _ : state) {
|
||||
benchmark::DoNotOptimize(bytes);
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
for (size_t j = 0; j < sizeof(NMEA_GGA) - 1; j++) {
|
||||
size_t len;
|
||||
if (reader.feed(bytes[j], len) != nullptr)
|
||||
total += len;
|
||||
}
|
||||
}
|
||||
benchmark::DoNotOptimize(total);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(LineReader_NmeaLine);
|
||||
|
||||
// --- FrameReader: a 30 byte frame behind a 4 byte header ---
|
||||
|
||||
static void FrameReader_30ByteFrame(benchmark::State &state) {
|
||||
FrameReader<sizeof(FRAME_30), 0xAA, 0xFF, 0x03, 0x00> reader;
|
||||
const uint8_t *bytes = FRAME_30;
|
||||
uint32_t frames = 0;
|
||||
|
||||
for (auto _ : state) {
|
||||
benchmark::DoNotOptimize(bytes);
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
for (size_t j = 0; j < sizeof(FRAME_30); j++) {
|
||||
if (reader.feed(bytes[j]) != nullptr)
|
||||
frames++;
|
||||
}
|
||||
}
|
||||
benchmark::DoNotOptimize(frames);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FrameReader_30ByteFrame);
|
||||
|
||||
// --- FrameReader: 4 byte checksummed frames, skipping noise and resyncing on a bad checksum ---
|
||||
|
||||
static void FrameReader_UltrasonicNoisy(benchmark::State &state) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
const uint8_t *bytes = ULTRASONIC_NOISY;
|
||||
uint32_t frames = 0;
|
||||
|
||||
for (auto _ : state) {
|
||||
benchmark::DoNotOptimize(bytes);
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
for (size_t j = 0; j < sizeof(ULTRASONIC_NOISY); j++) {
|
||||
const uint8_t *frame = reader.feed(bytes[j]);
|
||||
if (frame == nullptr)
|
||||
continue;
|
||||
if (frame[3] == static_cast<uint8_t>(frame[0] + frame[1] + frame[2])) {
|
||||
frames++;
|
||||
} else {
|
||||
reader.resync();
|
||||
}
|
||||
}
|
||||
}
|
||||
benchmark::DoNotOptimize(frames);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FrameReader_UltrasonicNoisy);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Serves a fixed byte stream as received data. Every write rewinds it, so the stream answers each request.
|
||||
class FakeUART : public uart::UARTComponent {
|
||||
public:
|
||||
void load(const uint8_t *data, size_t len) {
|
||||
this->data_ = data;
|
||||
this->len_ = len;
|
||||
this->pos_ = 0;
|
||||
}
|
||||
void rewind() { this->pos_ = 0; }
|
||||
|
||||
void write_array(const uint8_t *data, size_t len) override { this->rewind(); }
|
||||
bool peek_byte(uint8_t *data) override {
|
||||
if (this->pos_ >= this->len_)
|
||||
return false;
|
||||
*data = this->data_[this->pos_];
|
||||
return true;
|
||||
}
|
||||
bool read_array(uint8_t *data, size_t len) override {
|
||||
if (len > this->len_ - this->pos_)
|
||||
return false;
|
||||
std::memcpy(data, this->data_ + this->pos_, len);
|
||||
this->pos_ += len;
|
||||
return true;
|
||||
}
|
||||
size_t available() override { return this->len_ - this->pos_; }
|
||||
uart::UARTFlushResult flush() override { return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; }
|
||||
void check_logger_conflict() override {}
|
||||
|
||||
protected:
|
||||
const uint8_t *data_{nullptr};
|
||||
size_t len_{0};
|
||||
size_t pos_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -19,21 +19,9 @@ from esphome.const import (
|
||||
CONF_NOTIFY,
|
||||
CONF_SERVICE_UUID,
|
||||
CONF_TYPE,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..types import SetCoreConfigCallable
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def esp32_platform(set_core_config: SetCoreConfigCallable) -> None:
|
||||
# The raw-gattc node family gates through BLE_CLIENT_SCHEMA's
|
||||
# _legacy_engine_only choke point; these schema tests exercise the esp32 arm.
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
|
||||
DESCRIPTOR_CONFIG: ConfigType = {
|
||||
CONF_NAME: "test",
|
||||
CONF_SERVICE_UUID: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E",
|
||||
@@ -96,72 +84,3 @@ def test_on_notify_implies_notify() -> None:
|
||||
def test_notify_unchanged_without_on_notify() -> None:
|
||||
config: ConfigType = {CONF_NOTIFY: False}
|
||||
assert notify_from_on_notify(config)[CONF_NOTIFY] is False
|
||||
|
||||
|
||||
def test_legacy_node_choke_point_rejects_other_platforms(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
from esphome.components import ble_client
|
||||
from esphome.core import ID
|
||||
|
||||
set_core_config(PlatformFramework.RP2_ARDUINO)
|
||||
with pytest.raises(cv.Invalid, match="not been migrated"):
|
||||
ble_client._legacy_engine_only(ID("x"))
|
||||
# Through the public schema too, so removing the cv.All wiring fails here.
|
||||
with pytest.raises(cv.Invalid, match="not been migrated"):
|
||||
ble_client.BLE_CLIENT_SCHEMA({})
|
||||
|
||||
|
||||
def test_neutral_arm_rejects_esp32_only_keys(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
# Pins the schema split's rejection side: the legacy-only keys must not
|
||||
# leak into the neutral arm. The hub is registered so the extra key is
|
||||
# the only error - without it the missing-tracker error would satisfy
|
||||
# the raises vacuously.
|
||||
from esphome.components import ble_client, ble_device_base
|
||||
|
||||
set_core_config(PlatformFramework.RP2_ARDUINO)
|
||||
ble_device_base.register_hub_provider("rp2_ble_tracker")
|
||||
CORE.loaded_integrations.add("rp2_ble_tracker")
|
||||
for key in ("name", "on_passkey_request", "on_passkey_notification"):
|
||||
with pytest.raises(cv.Invalid, match="extra keys not allowed"):
|
||||
ble_client.CONFIG_SCHEMA({"mac_address": "AA:BB:CC:DD:EE:FF", key: "x"})
|
||||
|
||||
|
||||
def test_security_actions_reject_platforms_without_the_feature(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
from esphome.components import ble_client
|
||||
|
||||
set_core_config(PlatformFramework.RP2_ARDUINO)
|
||||
for schema in (
|
||||
ble_client.BLE_PASSKEY_REPLY_ACTION_SCHEMA,
|
||||
ble_client.BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA,
|
||||
ble_client.BLE_REMOVE_BOND_ACTION_SCHEMA,
|
||||
):
|
||||
with pytest.raises(cv.Invalid, match="'security' feature, which rp2"):
|
||||
schema({})
|
||||
|
||||
|
||||
def test_node_schema_passes_on_every_gatt_platform(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
# The neutral node schema carries no engine gate: raw_gattc components
|
||||
# stay choked, gatt_node components validate wherever ble_client does.
|
||||
from esphome.components import ble_client
|
||||
|
||||
for pf in (PlatformFramework.ESP32_IDF, PlatformFramework.RP2_ARDUINO):
|
||||
set_core_config(pf)
|
||||
assert ble_client.NODE_BLE_CLIENT_SCHEMA({})
|
||||
|
||||
|
||||
def test_feature_error_names_the_available_features(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
from esphome.components import ble_client
|
||||
from esphome.core import ID
|
||||
|
||||
set_core_config(PlatformFramework.RP2_ARDUINO)
|
||||
with pytest.raises(cv.Invalid, match="provides: gatt_node"):
|
||||
ble_client._legacy_engine_only(ID("x"))
|
||||
Whitespace-only changes.
@@ -1,79 +0,0 @@
|
||||
"""Tests for the cross-component GATT slot ledger."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components import (
|
||||
ble_client,
|
||||
ble_device_base,
|
||||
bluetooth_connection,
|
||||
bluetooth_proxy,
|
||||
rp2040_ble,
|
||||
)
|
||||
from esphome.const import CONF_MAC_ADDRESS, PlatformFramework
|
||||
from esphome.core import CORE
|
||||
|
||||
from ..types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def test_gatt_slot_ledger_rejects_overcommit_on_rp2(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
# rp2 owns its budget: the stack's validation reports the overcommit and
|
||||
# the neutral cap check stays silent (one message per misconfiguration).
|
||||
set_core_config(PlatformFramework.RP2_ARDUINO)
|
||||
bluetooth_connection.consume_gatt_slot("bluetooth_proxy", 3)({})
|
||||
bluetooth_connection.consume_gatt_slot("ble_client")({})
|
||||
bluetooth_connection.FINAL_VALIDATE_SCHEMA({})
|
||||
with pytest.raises(cv.Invalid, match="rp2 maximum is 3"):
|
||||
rp2040_ble.validate_connection_slots()
|
||||
|
||||
|
||||
def test_gatt_slot_ledger_skipped_in_testing_mode(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
# Grouped component builds merge fixtures past the cap; the check defers
|
||||
# to testing mode like esp32_ble.validate_connection_slots.
|
||||
set_core_config(PlatformFramework.RP2_ARDUINO)
|
||||
bluetooth_connection.consume_gatt_slot("bluetooth_proxy", 3)({})
|
||||
bluetooth_connection.consume_gatt_slot("ble_client")({})
|
||||
CORE.testing_mode = True
|
||||
try:
|
||||
bluetooth_connection.FINAL_VALIDATE_SCHEMA({})
|
||||
rp2040_ble.validate_connection_slots()
|
||||
finally:
|
||||
CORE.testing_mode = False
|
||||
|
||||
|
||||
def test_real_validators_charge_the_ledger_on_rp2(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
# End to end through the component CONFIG_SCHEMAs (no hand charges):
|
||||
# removing either consumer's consume_gatt_slot call fails this test.
|
||||
set_core_config(PlatformFramework.RP2_ARDUINO)
|
||||
ble_device_base.register_hub_provider("rp2_ble_tracker")
|
||||
CORE.loaded_integrations.add("rp2_ble_tracker")
|
||||
bluetooth_proxy.CONFIG_SCHEMA({})
|
||||
ble_client.CONFIG_SCHEMA({CONF_MAC_ADDRESS: "AA:BB:CC:DD:EE:FF"})
|
||||
# The proxy defaults to 3 slots on rp2; ble_client's claim overcommits
|
||||
# and rp2's own budget names every claimant.
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match="Components: bluetooth_proxy, bluetooth_proxy, bluetooth_proxy, "
|
||||
"ble_client",
|
||||
):
|
||||
rp2040_ble.validate_connection_slots()
|
||||
|
||||
|
||||
def test_neutral_cap_check_guards_future_hub_platforms(
|
||||
set_core_config: SetCoreConfigCallable, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
# Both current platforms defer to their stack budgets; pin the message
|
||||
# and boundary of the branch a future budget-less hub platform takes.
|
||||
set_core_config(PlatformFramework.RP2_ARDUINO)
|
||||
monkeypatch.setattr(bluetooth_connection, "_STACK_BUDGETS", {})
|
||||
bluetooth_connection.consume_gatt_slot("bluetooth_proxy", 3)({})
|
||||
bluetooth_connection.FINAL_VALIDATE_SCHEMA({})
|
||||
bluetooth_connection.consume_gatt_slot("ble_client")({})
|
||||
with pytest.raises(cv.Invalid, match="supports at most 3 GATT client connection"):
|
||||
bluetooth_connection.FINAL_VALIDATE_SCHEMA({})
|
||||
@@ -186,18 +186,6 @@ def test_rp2_rejects_esp32_only_keys_by_name(
|
||||
bluetooth_proxy.CONFIG_SCHEMA({"connections": [{}]})
|
||||
|
||||
|
||||
def test_esp32_explicit_connections_claim_gatt_slots(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
# Explicit `connections:` entries must charge the slot ledger like the
|
||||
# generated ones; dev historically let them evade the budget.
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
bluetooth_proxy.CONFIG_SCHEMA({"active": True, "connections": [{}, {}]})
|
||||
# Exact match (one entry per slot): catches a missed charge and a
|
||||
# double charge alike.
|
||||
assert bluetooth_connection._get_data().slot_consumers == ["bluetooth_proxy"] * 2
|
||||
|
||||
|
||||
def test_hub_source_filter_covers_every_hub_platform() -> None:
|
||||
# bluetooth_connection cannot import this module to derive the hub.cpp
|
||||
# framework set, so pin it here: a platform admitted to the proxy but
|
||||
@@ -241,12 +229,6 @@ def test_every_registered_hub_platform_has_a_schema_arm() -> None:
|
||||
# Hub platforms must also be in the backend registry the shared codegen
|
||||
# helpers dispatch on.
|
||||
assert registered <= set(bluetooth_connection._PLATFORM_BACKENDS)
|
||||
# Every non-esp32 backend platform must carry a slot cap: without one the
|
||||
# ledger's FINAL_VALIDATE accepts unlimited claims silently (esp32's cap
|
||||
# is the controller budget in esp32_ble).
|
||||
assert set(bluetooth_connection._PLATFORM_BACKENDS) - {"esp32"} <= set(
|
||||
bluetooth_connection.HUB_MAX_CONNECTIONS
|
||||
)
|
||||
# The outer walkable schema's bound must stay the loosest platform cap.
|
||||
assert (
|
||||
max(bluetooth_connection.HUB_MAX_CONNECTIONS.values())
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Tests for cc1101 packet_length validation."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.cc1101 import _validate_packet_mode
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import Lambda
|
||||
|
||||
LENGTH_LAMBDA = Lambda("return x.size() < 3 ? 0 : x[2] + 12;")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{"packet_mode": True, "gdo0_pin": 2, "packet_length": LENGTH_LAMBDA},
|
||||
{"packet_mode": True, "gdo0_pin": 2, "packet_length": 1024},
|
||||
{"packet_mode": True, "gdo0_pin": 2, "packet_length": 64, "crc_enable": True},
|
||||
{"packet_mode": False, "packet_length": 200, "crc_enable": True},
|
||||
],
|
||||
)
|
||||
def test_packet_length_valid(config: dict[str, Any]) -> None:
|
||||
"""Fixed lengths, a lambda, and CRC on packets that fit the FIFO are accepted."""
|
||||
assert _validate_packet_mode(config) is config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "message"),
|
||||
[
|
||||
(
|
||||
{"packet_mode": False, "packet_length": LENGTH_LAMBDA},
|
||||
"packet_mode is required when packet_length is a lambda",
|
||||
),
|
||||
(
|
||||
{"packet_mode": True, "gdo0_pin": 2},
|
||||
"packet_length is required when packet_mode is enabled",
|
||||
),
|
||||
(
|
||||
{
|
||||
"packet_mode": True,
|
||||
"gdo0_pin": 2,
|
||||
"packet_length": 65,
|
||||
"crc_enable": True,
|
||||
},
|
||||
"crc_enable is not supported",
|
||||
),
|
||||
(
|
||||
{
|
||||
"packet_mode": True,
|
||||
"gdo0_pin": 2,
|
||||
"packet_length": LENGTH_LAMBDA,
|
||||
"crc_enable": True,
|
||||
},
|
||||
"crc_enable is not supported",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_packet_length_invalid(config: dict[str, Any], message: str) -> None:
|
||||
"""Invalid packet_length combinations are rejected with a clear message."""
|
||||
with pytest.raises(cv.Invalid, match=message):
|
||||
_validate_packet_mode(config)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Validate the sync-word limit before firmware generation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.sx126x import CONF_SYNC_VALUE, CONFIG_SCHEMA
|
||||
import esphome.config_validation as cv
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [0, 1, 2, 8])
|
||||
def test_supported_sync_lengths(size: int) -> None:
|
||||
value = [0x14] * size
|
||||
assert CONFIG_SCHEMA.schema[CONF_SYNC_VALUE](value) == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [9, 256])
|
||||
def test_oversized_sync_word(size: int) -> None:
|
||||
with pytest.raises(cv.Invalid, match="at most 8"):
|
||||
CONFIG_SCHEMA.schema[CONF_SYNC_VALUE]([0x14] * size)
|
||||
@@ -1,28 +0,0 @@
|
||||
ble_client:
|
||||
- mac_address: 01:02:03:04:05:06
|
||||
id: test_blec
|
||||
on_connect:
|
||||
then:
|
||||
- ble_client.ble_write:
|
||||
id: test_blec
|
||||
service_uuid: '1802'
|
||||
characteristic_uuid: '2a06'
|
||||
value: [0x04, 0x05, 0x06]
|
||||
on_disconnect:
|
||||
then:
|
||||
# Reconnect from the disconnect trigger: the connect action must
|
||||
# survive the same dispatch that started it, so the log still runs.
|
||||
- ble_client.connect: test_blec
|
||||
- logger.log: reconnected
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Connect button
|
||||
on_press:
|
||||
- ble_client.connect: test_blec
|
||||
- ble_client.ble_write:
|
||||
id: test_blec
|
||||
service_uuid: '1802'
|
||||
characteristic_uuid: '2a06'
|
||||
value: !lambda return {0x01, 0x02};
|
||||
- ble_client.disconnect: test_blec
|
||||
@@ -1,6 +0,0 @@
|
||||
# The neutral engine: the BTstack backend and rp2040_ble come in through
|
||||
# bluetooth_connection's auto-load; the tracker hub supplies the sightings.
|
||||
packages:
|
||||
common: !include common-gatt.yaml
|
||||
|
||||
rp2_ble_tracker:
|
||||
@@ -10,9 +10,6 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# and the listener vector it dispatches into (codegen-sized by consumers).
|
||||
async def to_code_testing(config):
|
||||
cg.add_define("USE_BLE_DEVICE_IRK")
|
||||
# The gatt contract test exercises the gated lookup helpers; compile
|
||||
# their definitions (ble_gatt_client.cpp) into the test build.
|
||||
cg.add_define("USE_BLE_GATT_CLIENT")
|
||||
cg.add_define("USE_BLE_SCAN_RESPONSE_MERGER")
|
||||
cg.add_define("ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT", 4)
|
||||
|
||||
|
||||
@@ -79,54 +79,4 @@ TEST(BleGattClientContract, MinimalImplementerCompilesAndRoutesEvents) {
|
||||
EXPECT_EQ(table.descriptor_count, 0);
|
||||
}
|
||||
|
||||
// A radon_eye_rd200-shaped table: two services, the second holding a
|
||||
// notifying characteristic with a CCCD and a bare write characteristic.
|
||||
class ServiceTableLookup : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->services_[0] = {ESPBTUUID::from_uint16(0x1800), 0x0001, 0x0005, 0, 1};
|
||||
this->services_[1] = {ESPBTUUID::from_uint16(0x1523), 0x0010, 0x0020, 1, 2};
|
||||
this->characteristics_[0] = {ESPBTUUID::from_uint16(0x2A00), 0x0003, 0x0003, 0x02, 0, 0};
|
||||
this->characteristics_[1] = {ESPBTUUID::from_uint16(0x1525), 0x0012, 0x0014, 0x10, 0, 1};
|
||||
this->characteristics_[2] = {ESPBTUUID::from_uint16(0x1524), 0x0016, 0x0016, 0x04, 1, 0};
|
||||
this->descriptors_[0] = {ESPBTUUID::from_uint16(0x2902), 0x0013};
|
||||
this->table_ = {this->services_, this->characteristics_, this->descriptors_, 2, 3, 1};
|
||||
}
|
||||
|
||||
GattService services_[2];
|
||||
GattCharacteristic characteristics_[3];
|
||||
GattDescriptor descriptors_[1];
|
||||
GattServiceTable table_;
|
||||
};
|
||||
|
||||
TEST_F(ServiceTableLookup, FindsServicesAndCharacteristicsByUuid) {
|
||||
const GattService *service = find_service(this->table_, ESPBTUUID::from_uint16(0x1523));
|
||||
ASSERT_NE(service, nullptr);
|
||||
EXPECT_EQ(service->start_handle, 0x0010);
|
||||
EXPECT_EQ(find_service(this->table_, ESPBTUUID::from_uint16(0xFFFF)), nullptr);
|
||||
|
||||
const GattCharacteristic *characteristic =
|
||||
find_characteristic(this->table_, *service, ESPBTUUID::from_uint16(0x1525));
|
||||
ASSERT_NE(characteristic, nullptr);
|
||||
EXPECT_EQ(characteristic->value_handle, 0x0012);
|
||||
// The lookup is scoped to the service: 0x2A00 lives in the other service.
|
||||
EXPECT_EQ(find_characteristic(this->table_, *service, ESPBTUUID::from_uint16(0x2A00)), nullptr);
|
||||
}
|
||||
|
||||
TEST_F(ServiceTableLookup, FindsTheCccdAndReportsItsAbsence) {
|
||||
const GattService *service = find_service(this->table_, ESPBTUUID::from_uint16(0x1523));
|
||||
const GattCharacteristic *notify_char = find_characteristic(this->table_, *service, ESPBTUUID::from_uint16(0x1525));
|
||||
EXPECT_EQ(find_cccd(this->table_, *notify_char), 0x0013);
|
||||
const GattCharacteristic *write_char = find_characteristic(this->table_, *service, ESPBTUUID::from_uint16(0x1524));
|
||||
EXPECT_EQ(find_cccd(this->table_, *write_char), 0);
|
||||
}
|
||||
|
||||
TEST_F(ServiceTableLookup, RejectsRangesThatOverrunTheTable) {
|
||||
// A corrupt index range must fail the lookup, not walk out of bounds.
|
||||
GattService bad_service = {ESPBTUUID::from_uint16(0x1523), 0x0010, 0x0020, 2, 5};
|
||||
EXPECT_EQ(find_characteristic(this->table_, bad_service, ESPBTUUID::from_uint16(0x1524)), nullptr);
|
||||
GattCharacteristic bad_char = {ESPBTUUID::from_uint16(0x1525), 0x0012, 0x0014, 0x10, 0, 9};
|
||||
EXPECT_EQ(find_cccd(this->table_, bad_char), 0);
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
@@ -1,6 +1,5 @@
|
||||
# Advertisement-only proxy on esp32 by explicit choice: no GATT backend is
|
||||
# compiled (USE_BLE_GATT_CLIENT unset), which pins the
|
||||
# USE_BLUETOOTH_PROXY_CONNECTIONS gating and the
|
||||
# compiled (USE_BLE_GATT_CLIENT unset), which pins the HAS_GATT gating and the
|
||||
# address-scoped maintenance path that a connections build never exercises.
|
||||
# Under batch grouping the active default build is what runs; the standalone
|
||||
# compile of this fixture is what exercises the passive gating.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
|
||||
cc1101:
|
||||
# Above 255 bytes: infinite length mode, then fixed for the remainder
|
||||
- id: transceiver_long_fixed
|
||||
cs_pin: GPIO5
|
||||
gdo0_pin: GPIO4
|
||||
packet_mode: true
|
||||
packet_length: 1024
|
||||
on_packet:
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("cc1101", "long packet %u bytes", x.size());'
|
||||
|
||||
# Length taken from the packet header
|
||||
- id: transceiver_long_lambda
|
||||
cs_pin: GPIO15
|
||||
gdo0_pin: GPIO2
|
||||
packet_mode: true
|
||||
packet_length: !lambda |-
|
||||
if (x.size() < 3) return 0;
|
||||
if (x[0] != 0x54) return -1;
|
||||
return x[2] + 12;
|
||||
on_packet:
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("cc1101", "lambda packet %u bytes rssi %.1f", x.size(), rssi);'
|
||||
@@ -0,0 +1,24 @@
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
|
||||
|
||||
cc1101:
|
||||
- id: transceiver_long_fixed
|
||||
cs_pin: GPIO5
|
||||
gdo0_pin: GPIO4
|
||||
packet_mode: true
|
||||
packet_length: 1024
|
||||
on_packet:
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("cc1101", "long packet %u bytes", x.size());'
|
||||
|
||||
- id: transceiver_long_lambda
|
||||
cs_pin: GPIO15
|
||||
gdo0_pin: GPIO2
|
||||
packet_mode: true
|
||||
packet_length: !lambda |-
|
||||
if (x.size() < 3) return 0;
|
||||
if (x[0] != 0x54) return -1;
|
||||
return x[2] + 12;
|
||||
on_packet:
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("cc1101", "lambda packet %u bytes rssi %.1f", x.size(), rssi);'
|
||||
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO5
|
||||
gdo0_pin: GPIO4
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
|
||||
cc1101: !include common.yaml
|
||||
@@ -20,7 +20,14 @@ class FakePin final : public GPIOPin {
|
||||
bool level{false};
|
||||
};
|
||||
|
||||
// Answers the 8-bit status read with a set report and 16-bit setup reads with zeros.
|
||||
// Fills a 16-bit register read: the firmware check code read gets a valid code, other registers zeros.
|
||||
inline void answer_setup_read(const uint8_t *data, size_t size, uint8_t *out, size_t out_size) {
|
||||
std::memset(out, 0, out_size);
|
||||
if (size == 2 && encode_uint16(data[0], data[1]) == cstxxx::REG_FW_CHECK_CODE && out_size == 4)
|
||||
out[2] = out[3] = 0xCA;
|
||||
}
|
||||
|
||||
// Answers the 8-bit status read with a set report and 16-bit setup reads through answer_setup_read().
|
||||
class StatusBus final : public i2c::I2CBus {
|
||||
public:
|
||||
std::array<uint8_t, 28> status{};
|
||||
@@ -35,7 +42,7 @@ class StatusBus final : public i2c::I2CBus {
|
||||
if (size == 1 && data[0] == CST226_REG_STATUS) {
|
||||
std::memcpy(out, this->status.data(), std::min(out_size, this->status.size()));
|
||||
} else {
|
||||
std::memset(out, 0, out_size);
|
||||
answer_setup_read(data, size, out, out_size);
|
||||
}
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/cst226/touchscreen/cst226_touchscreen.h"
|
||||
#include "../common.h"
|
||||
|
||||
@@ -14,6 +17,45 @@ TEST(CST226TouchscreenTest, SetupCompletes) {
|
||||
EXPECT_FALSE(hub.is_failed());
|
||||
}
|
||||
|
||||
// Records the writes that carry no read, and answers reads through answer_setup_read().
|
||||
class WriteLogBus final : public i2c::I2CBus {
|
||||
public:
|
||||
std::vector<std::vector<uint8_t>> writes;
|
||||
bool bad_check_code{false};
|
||||
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *data, size_t size, uint8_t *out,
|
||||
size_t out_size) override {
|
||||
if (out_size == 0) {
|
||||
this->writes.emplace_back(data, data + size);
|
||||
} else {
|
||||
answer_setup_read(data, size, out, out_size);
|
||||
if (this->bad_check_code)
|
||||
std::memset(out, 0, out_size);
|
||||
}
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(CST226TouchscreenTest, SetupEntersCommandModeAndLeavesIt) {
|
||||
WriteLogBus bus;
|
||||
CST226Touchscreen hub;
|
||||
hub.set_i2c_bus(&bus);
|
||||
hub.setup();
|
||||
ASSERT_GE(bus.writes.size(), 2u);
|
||||
EXPECT_EQ(bus.writes.front(), (std::vector<uint8_t>{0xD1, 0x01}));
|
||||
EXPECT_EQ(bus.writes.back(), (std::vector<uint8_t>{0xD1, 0x09}));
|
||||
EXPECT_FALSE(hub.is_failed());
|
||||
}
|
||||
|
||||
TEST(CST226TouchscreenTest, SetupFailsOnAWrongCheckCode) {
|
||||
WriteLogBus bus;
|
||||
bus.bad_check_code = true;
|
||||
CST226Touchscreen hub;
|
||||
hub.set_i2c_bus(&bus);
|
||||
hub.setup();
|
||||
EXPECT_TRUE(hub.is_failed());
|
||||
}
|
||||
|
||||
TEST(CST226TouchscreenTest, SetupFailsWithoutTheChip) {
|
||||
StatusBus bus;
|
||||
bus.fail = true;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "esphome/components/cstxxx/cstxxx.h"
|
||||
|
||||
namespace esphome::cstxxx::testing {
|
||||
|
||||
TEST(CSTxxxDecodeTouch, FirstRecordStartsTheReport) {
|
||||
const uint8_t report[report_size(1)] = {0x16, 0x12, 0x34, 0x56, 0x78, 0x01, SYNC_BYTE};
|
||||
auto touch = decode_touch(report, 0);
|
||||
EXPECT_EQ(touch.id, 1);
|
||||
EXPECT_EQ(touch.event, 0x06);
|
||||
EXPECT_EQ(touch.x, 0x125);
|
||||
EXPECT_EQ(touch.y, 0x346);
|
||||
EXPECT_EQ(touch.z, 0x78);
|
||||
}
|
||||
|
||||
TEST(CSTxxxDecodeTouch, LaterRecordsSkipTheStatusBytes) {
|
||||
uint8_t report[report_size(3)]{};
|
||||
report[FINGER_COUNT_IDX] = 3;
|
||||
report[SYNC_IDX] = SYNC_BYTE;
|
||||
const uint8_t third[5] = {0x26, 0xFF, 0x00, 0xF0, 0x05};
|
||||
std::copy(third, third + 5, report + 12);
|
||||
auto touch = decode_touch(report, 2);
|
||||
EXPECT_EQ(touch.id, 2);
|
||||
EXPECT_EQ(touch.x, 0xFFF);
|
||||
EXPECT_EQ(touch.y, 0x000);
|
||||
EXPECT_EQ(touch.z, 5);
|
||||
}
|
||||
|
||||
} // namespace esphome::cstxxx::testing
|
||||
@@ -2,3 +2,7 @@ packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
# IPv6 with an IP sensor: compiles the IPv6 event handler's listener notification
|
||||
network:
|
||||
enable_ipv6: true
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Enable real logger callbacks for radio reentrancy regression tests."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.logger import request_log_listener
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
request_log_listener()
|
||||
# The tests build a software SPI bus in C++, which no configured bus asks for
|
||||
cg.add_define("USE_SPI_SOFTWARE")
|
||||
|
||||
manifest.multi_conf = False
|
||||
manifest.to_code = to_code
|
||||
@@ -7,3 +7,22 @@ button:
|
||||
cold: true
|
||||
- sx126x.set_mode_sleep:
|
||||
cold: !lambda return id(sx126x_my_number).state > 1;
|
||||
|
||||
- platform: template
|
||||
name: "SX126x Async Send or Cancel"
|
||||
on_press:
|
||||
- lambda: |-
|
||||
class Listener : public sx126x::SX126xTransmitListener {
|
||||
public:
|
||||
void on_transmit_complete(sx126x::SX126xError result) override {
|
||||
ESP_LOGD("sx126x_test", "Async completion: %d", static_cast<int>(result));
|
||||
}
|
||||
};
|
||||
static Listener listener;
|
||||
if (id(sx126x_radio).is_transmitting()) {
|
||||
id(sx126x_radio).cancel_transmit_async(&listener);
|
||||
} else {
|
||||
const uint8_t data[] = {0x11, 0x22, 0x33};
|
||||
auto result = id(sx126x_radio).start_transmit_async(&listener, data, sizeof(data), 4000);
|
||||
ESP_LOGD("sx126x_test", "Async admission: %d", static_cast<int>(result));
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
sx126x:
|
||||
id: sx126x_radio
|
||||
dio1_pin: ${dio1_pin}
|
||||
cs_pin: ${cs_pin}
|
||||
busy_pin: ${busy_pin}
|
||||
|
||||
@@ -0,0 +1,643 @@
|
||||
#include "sx126x_test_helpers.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <tuple>
|
||||
|
||||
namespace esphome::sx126x::testing {
|
||||
|
||||
class TransmitListener : public SX126xTransmitListener {
|
||||
public:
|
||||
void on_transmit_complete(SX126xError result) override {
|
||||
this->results.push_back(result);
|
||||
if (this->on_complete) {
|
||||
this->on_complete(result);
|
||||
}
|
||||
}
|
||||
std::vector<SX126xError> results;
|
||||
std::function<void(SX126xError)> on_complete;
|
||||
};
|
||||
|
||||
class ReceiveCallback : public SX126xListener {
|
||||
public:
|
||||
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override { this->on_packet_callback(); }
|
||||
std::function<void()> on_packet_callback;
|
||||
};
|
||||
|
||||
class SX126xAsyncModes : public ::testing::TestWithParam<std::tuple<bool, bool, bool>> {};
|
||||
|
||||
TEST_P(SX126xAsyncModes, ReturnsBeforeCompletionAndRestoresMode) {
|
||||
const auto [lora, rx, fixed] = GetParam();
|
||||
RadioHardware hw;
|
||||
TransmitListener listener;
|
||||
hw.configure(lora, rx, fixed);
|
||||
hw.auto_complete = false;
|
||||
hw.clear_trace();
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet, 1234), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_TRUE(hw.radio.is_transmitting());
|
||||
EXPECT_TRUE(listener.results.empty());
|
||||
EXPECT_EQ(hw.sends, 1u);
|
||||
const auto &tx = hw.transactions.back();
|
||||
EXPECT_EQ(tx, (std::vector<uint8_t>{RADIO_SET_TX, 0x01, 0x34, 0x80}));
|
||||
const auto fifo = std::find_if(hw.transactions.begin(), hw.transactions.end(),
|
||||
[](const auto &cmd) { return cmd[0] == RADIO_WRITE_BUFFER; });
|
||||
ASSERT_NE(fifo, hw.transactions.end());
|
||||
EXPECT_EQ(*fifo, (std::vector<uint8_t>{RADIO_WRITE_BUFFER, 0, 0x11, 0x22, 0x33}));
|
||||
hw.packet.assign(3, 0x99); // The driver must not retain the caller's payload.
|
||||
hw.irq = IRQ_TX_DONE;
|
||||
listener.on_complete = [&](SX126xError result) {
|
||||
EXPECT_FALSE(hw.radio.is_transmitting());
|
||||
EXPECT_EQ(hw.receiving, rx);
|
||||
EXPECT_TRUE(hw.cs.level);
|
||||
};
|
||||
hw.radio.loop();
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
}
|
||||
|
||||
TEST_P(SX126xAsyncModes, RejectsEveryFailedStartWithoutCallbackAndCanRetry) {
|
||||
const auto [lora, rx, fixed] = GetParam();
|
||||
RadioHardware reference;
|
||||
TransmitListener listener;
|
||||
reference.configure(lora, rx, fixed);
|
||||
reference.clear_trace();
|
||||
ASSERT_EQ(reference.radio.start_transmit_async(&listener, reference.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
const size_t count = reference.transactions.size();
|
||||
reference.radio.cancel_transmit_async(&listener);
|
||||
for (size_t fault = 1; fault <= count; fault++) {
|
||||
SCOPED_TRACE(fault);
|
||||
RadioHardware hw;
|
||||
TransmitListener result;
|
||||
hw.configure(lora, rx, fixed);
|
||||
hw.clear_trace();
|
||||
hw.fail_at = fault;
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&result, hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
|
||||
EXPECT_TRUE(result.results.empty());
|
||||
EXPECT_FALSE(hw.radio.is_transmitting());
|
||||
EXPECT_TRUE(hw.cs.level);
|
||||
EXPECT_TRUE(hw.transactions[fault - 1].empty());
|
||||
EXPECT_EQ(hw.sends, 0u);
|
||||
EXPECT_EQ(hw.receiving, rx); // One-shot fault permits failed-start cleanup.
|
||||
hw.clear_trace();
|
||||
hw.radio.configure();
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&result, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(result.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(SX126xAsyncModes, ReportsEveryCompletionAndCleanupFailureOnce) {
|
||||
const auto [lora, rx, fixed] = GetParam();
|
||||
RadioHardware reference;
|
||||
TransmitListener listener;
|
||||
reference.configure(lora, rx, fixed);
|
||||
ASSERT_EQ(reference.radio.start_transmit_async(&listener, reference.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
reference.clear_trace();
|
||||
reference.radio.loop();
|
||||
const size_t count = reference.transactions.size();
|
||||
for (size_t fault = 1; fault <= count; fault++) {
|
||||
SCOPED_TRACE(fault);
|
||||
RadioHardware hw;
|
||||
TransmitListener result;
|
||||
hw.configure(lora, rx, fixed);
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&result, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.clear_trace();
|
||||
hw.fail_at = fault;
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(result.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_BUSY_TIMEOUT}));
|
||||
EXPECT_TRUE(hw.transactions[fault - 1].empty());
|
||||
EXPECT_TRUE(hw.cs.level);
|
||||
EXPECT_FALSE(hw.radio.is_transmitting());
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(result.results.size(), 1u);
|
||||
hw.clear_trace();
|
||||
hw.radio.configure();
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&result, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(result.results.size(), 2u);
|
||||
EXPECT_EQ(result.results.back(), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_EQ(hw.receiving, rx);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(SX126xAsyncModes, BlockingAndAsyncUseTheSameCommandSequence) {
|
||||
const auto [lora, rx, fixed] = GetParam();
|
||||
RadioHardware blocking, async;
|
||||
TransmitListener listener;
|
||||
blocking.configure(lora, rx, fixed);
|
||||
async.configure(lora, rx, fixed);
|
||||
blocking.clear_trace();
|
||||
async.clear_trace();
|
||||
ASSERT_EQ(blocking.radio.transmit_packet(blocking.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
ASSERT_EQ(async.radio.start_transmit_async(&listener, async.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
async.radio.loop();
|
||||
EXPECT_EQ(blocking.transactions, async.transactions);
|
||||
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
EXPECT_EQ(blocking.receiving, async.receiving);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(LoRaAndFSKWithRxOrSleepAndFixedOrVariablePayload, SX126xAsyncModes,
|
||||
::testing::Combine(::testing::Bool(), ::testing::Bool(), ::testing::Bool()));
|
||||
|
||||
TEST(SX126xAsync, SoftwareDeadlineWithoutDioAndCachedTickBeforeStart) {
|
||||
RadioHardware hw;
|
||||
TransmitListener listener;
|
||||
hw.configure(true, true);
|
||||
hw.auto_complete = false;
|
||||
hw.dio.internal = true;
|
||||
const uint32_t before = millis();
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet, 50), SX126xError::SX126X_ERROR_NONE);
|
||||
const uint32_t after = millis();
|
||||
{
|
||||
LoopBlockingGuard clock{nullptr, nullptr, before - 1};
|
||||
hw.radio.call();
|
||||
EXPECT_TRUE(listener.results.empty()); // No unsigned underflow on the admitting loop tick.
|
||||
}
|
||||
{
|
||||
LoopBlockingGuard clock{nullptr, nullptr, after + 51};
|
||||
hw.radio.call(); // Still enabled despite no IRQ or ISR edge.
|
||||
}
|
||||
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_TIMEOUT}));
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
EXPECT_FALSE(hw.radio.is_transmitting());
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, UnrelatedIrqIsNotSuccessAndTimeoutWinsOverTxDone) {
|
||||
RadioHardware hw;
|
||||
TransmitListener listener;
|
||||
hw.configure(true, true);
|
||||
hw.auto_complete = false;
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
{
|
||||
LoopBlockingGuard clock{nullptr, nullptr, millis()};
|
||||
hw.radio.loop();
|
||||
}
|
||||
EXPECT_TRUE(listener.results.empty());
|
||||
hw.irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT;
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_TIMEOUT}));
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, CancelRequiresOwnerAndCompletesSynchronouslyOnce) {
|
||||
RadioHardware hw;
|
||||
TransmitListener listener, other;
|
||||
hw.configure(true, true);
|
||||
hw.auto_complete = false;
|
||||
EXPECT_FALSE(hw.radio.cancel_transmit_async(&listener));
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_FALSE(hw.radio.cancel_transmit_async(nullptr));
|
||||
EXPECT_FALSE(hw.radio.cancel_transmit_async(&other));
|
||||
EXPECT_TRUE(hw.radio.cancel_transmit_async(&listener));
|
||||
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_CANCELLED}));
|
||||
EXPECT_FALSE(hw.radio.cancel_transmit_async(&listener));
|
||||
hw.irq = IRQ_TX_DONE; // A late IRQ cannot deliver a second completion.
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(listener.results.size(), 1u);
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
hw.irq = IRQ_TX_DONE; // Also cleared before a new send starts.
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&other, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
{
|
||||
LoopBlockingGuard clock{nullptr, nullptr, millis()};
|
||||
hw.radio.loop();
|
||||
}
|
||||
EXPECT_TRUE(other.results.empty());
|
||||
EXPECT_TRUE(hw.radio.cancel_transmit_async(&other));
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, CompletionCallbackCanStartNextSend) {
|
||||
RadioHardware hw;
|
||||
TransmitListener first, second;
|
||||
hw.configure(true, true);
|
||||
hw.auto_complete = false;
|
||||
first.on_complete = [&](SX126xError result) {
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&second, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
};
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&first, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.irq = IRQ_TX_DONE;
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(first.results.size(), 1u);
|
||||
EXPECT_TRUE(hw.radio.is_transmitting());
|
||||
EXPECT_TRUE(second.results.empty());
|
||||
hw.irq = IRQ_TX_DONE;
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(second.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
}
|
||||
|
||||
static void expect_conflicting_calls_rejected(RadioHardware &hw, TransmitListener &listener) {
|
||||
const size_t count = hw.transactions.size();
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_BUSY);
|
||||
hw.radio.set_mode_rx();
|
||||
hw.radio.set_mode_tx();
|
||||
hw.radio.set_mode_standby(STDBY_XOSC);
|
||||
hw.radio.set_mode_sleep(true);
|
||||
hw.radio.configure();
|
||||
hw.radio.run_image_cal();
|
||||
EXPECT_EQ(hw.transactions.size(), count);
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, PendingReceiveRejectsAdmissionWithoutDispatchOrAcknowledgment) {
|
||||
RadioHardware hw;
|
||||
TransmitListener listener;
|
||||
PacketListener receiver;
|
||||
hw.configure(true, true);
|
||||
hw.radio.register_listener(&receiver);
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_BUSY);
|
||||
EXPECT_TRUE(receiver.packets.empty());
|
||||
EXPECT_TRUE(listener.results.empty());
|
||||
EXPECT_EQ(hw.irq, IRQ_RX_DONE);
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(receiver.packets.size(), 1u);
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
expect_conflicting_calls_rejected(hw, listener);
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(listener.results.size(), 1u);
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, ValidatesReadinessPayloadAndTimeoutWithoutSpi) {
|
||||
RadioHardware hw;
|
||||
TransmitListener listener;
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
hw.configure(true, true, true);
|
||||
hw.clear_trace();
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(nullptr, hw.packet), SX126xError::SX126X_ERROR_INVALID_PARAMS);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, nullptr, 3), SX126xError::SX126X_ERROR_INVALID_PARAMS);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet.data(), 0), SX126xError::SX126X_ERROR_INVALID_PARAMS);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet.data(), 2), SX126xError::SX126X_ERROR_INVALID_PARAMS);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet.data(), 256), SX126xError::SX126X_ERROR_INVALID_PARAMS);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet, 0), SX126xError::SX126X_ERROR_INVALID_PARAMS);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet, 262144), SX126xError::SX126X_ERROR_INVALID_PARAMS);
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
EXPECT_TRUE(listener.results.empty());
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet, 262143), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_EQ(hw.transactions.back(), (std::vector<uint8_t>{RADIO_SET_TX, 0xFF, 0xFF, 0xC0}));
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet, 1), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_EQ(hw.transactions.back(), (std::vector<uint8_t>{RADIO_SET_TX, 0, 0, 64}));
|
||||
hw.radio.loop();
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, PersistentBusyReportsFailedCleanupAndAllowsLaterRecovery) {
|
||||
RadioHardware hw;
|
||||
TransmitListener listener;
|
||||
hw.configure(true, true);
|
||||
hw.auto_complete = false;
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.busy.on_read = []() { return true; };
|
||||
ASSERT_TRUE(hw.radio.cancel_transmit_async(&listener));
|
||||
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_BUSY_TIMEOUT}));
|
||||
EXPECT_FALSE(hw.radio.is_transmitting());
|
||||
EXPECT_TRUE(hw.radio.status_has_warning());
|
||||
EXPECT_TRUE(hw.cs.level);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
EXPECT_EQ(listener.results.size(), 1u); // Failed admission never invokes completion.
|
||||
hw.busy.on_read = []() { return false; };
|
||||
hw.radio.configure();
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.irq = IRQ_TX_DONE;
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(listener.results.size(), 2u);
|
||||
EXPECT_EQ(listener.results.back(), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
EXPECT_FALSE(hw.radio.status_has_warning());
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, CancellationCallbackMayStartAnotherSend) {
|
||||
RadioHardware hw;
|
||||
TransmitListener first, second;
|
||||
hw.configure(true, true);
|
||||
hw.auto_complete = false;
|
||||
first.on_complete = [&](SX126xError result) {
|
||||
EXPECT_EQ(result, SX126xError::SX126X_ERROR_CANCELLED);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&second, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
};
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&first, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_TRUE(hw.radio.cancel_transmit_async(&first));
|
||||
EXPECT_TRUE(hw.radio.is_transmitting());
|
||||
EXPECT_FALSE(hw.radio.cancel_transmit_async(&first));
|
||||
hw.irq = IRQ_TX_DONE;
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(second.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, RejectsAfterFailedSetupWithoutBusAccess) {
|
||||
RadioHardware hw;
|
||||
TransmitListener listener;
|
||||
hw.fail_at = 1;
|
||||
hw.configure(true, true);
|
||||
ASSERT_TRUE(hw.radio.is_failed());
|
||||
hw.clear_trace();
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
EXPECT_TRUE(listener.results.empty());
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, TwoRadiosHaveIndependentOwnersAndCompletion) {
|
||||
RadioHardware a, b;
|
||||
TransmitListener first, second;
|
||||
a.configure(true, true);
|
||||
b.configure(false, false);
|
||||
a.auto_complete = b.auto_complete = false;
|
||||
ASSERT_EQ(a.radio.start_transmit_async(&first, a.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
ASSERT_EQ(b.radio.start_transmit_async(&second, b.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
a.radio.cancel_transmit_async(&first);
|
||||
EXPECT_TRUE(b.radio.is_transmitting());
|
||||
EXPECT_TRUE(second.results.empty());
|
||||
b.irq = IRQ_TX_DONE;
|
||||
b.radio.loop();
|
||||
EXPECT_EQ(first.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_CANCELLED}));
|
||||
EXPECT_EQ(second.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, BackToBackReceiveEchoPreservesBorrowedPacketForAllListeners) {
|
||||
class EchoListener : public SX126xListener {
|
||||
public:
|
||||
std::function<void(const std::vector<uint8_t> &)> callback;
|
||||
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override { callback(packet); }
|
||||
} echo;
|
||||
RadioHardware hw;
|
||||
TransmitListener tx;
|
||||
PacketListener observer;
|
||||
hw.configure(true, true);
|
||||
hw.auto_complete = false;
|
||||
hw.radio.register_listener(&echo);
|
||||
hw.radio.register_listener(&observer);
|
||||
const auto first = hw.packet;
|
||||
const std::vector<uint8_t> second(200, 0xA5);
|
||||
unsigned calls = 0;
|
||||
echo.callback = [&](const std::vector<uint8_t> &packet) {
|
||||
if (++calls == 1) {
|
||||
hw.packet = second; // Larger back-to-back RX could reallocate the driver's packet_.
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
const auto count = hw.transactions.size();
|
||||
hw.radio.loop(); // Explicit loop reentry must not overwrite the borrowed RX buffer either.
|
||||
// The send stops in standby and holds the new packet instead of overwriting the FIFO.
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&tx, packet), SX126xError::SX126X_ERROR_BUSY);
|
||||
ASSERT_EQ(hw.transactions.size(), count + 2);
|
||||
EXPECT_EQ(hw.transactions[count][0], RADIO_SET_STANDBY);
|
||||
EXPECT_EQ(hw.transactions.back()[0], RADIO_GET_IRQSTATUS);
|
||||
EXPECT_EQ(packet, first);
|
||||
} else {
|
||||
EXPECT_EQ(packet, second);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&tx, packet), SX126xError::SX126X_ERROR_NONE);
|
||||
}
|
||||
};
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(observer.packets.size(), 1u);
|
||||
EXPECT_EQ(observer.packets[0], first);
|
||||
EXPECT_TRUE(tx.results.empty());
|
||||
EXPECT_FALSE(hw.radio.is_transmitting());
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(observer.packets.size(), 2u);
|
||||
EXPECT_EQ(observer.packets[1], second);
|
||||
ASSERT_EQ(calls, 2u);
|
||||
auto fifo = std::find_if(hw.transactions.rbegin(), hw.transactions.rend(),
|
||||
[](const auto &cmd) { return !cmd.empty() && cmd[0] == RADIO_WRITE_BUFFER; });
|
||||
ASSERT_NE(fifo, hw.transactions.rend());
|
||||
EXPECT_EQ(std::vector<uint8_t>(fifo->begin() + 2, fifo->end()), second);
|
||||
hw.irq = IRQ_TX_DONE;
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(tx.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, FailedReconfigurationRejectsAsyncUntilReconfigured) {
|
||||
RadioHardware hw;
|
||||
TransmitListener listener;
|
||||
hw.configure(true, true);
|
||||
hw.clear_trace();
|
||||
hw.fail_at = 3;
|
||||
hw.radio.configure();
|
||||
hw.clear_trace();
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
EXPECT_TRUE(listener.results.empty());
|
||||
hw.radio.configure();
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, BlockingSendUsesIrqResultInsteadOfTreatingAnyDioAsSuccess) {
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
auto write = hw.cs.on_write;
|
||||
hw.cs.on_write = [&](bool high) {
|
||||
write(high);
|
||||
if (high && !hw.transactions.back().empty() && hw.transactions.back()[0] == RADIO_SET_TX) {
|
||||
hw.irq = IRQ_RX_TX_TIMEOUT;
|
||||
}
|
||||
};
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_TIMEOUT);
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, FailedAdmissionRestoresReceivePollingWithoutAnotherTransmission) {
|
||||
RadioHardware hw;
|
||||
PacketListener rx;
|
||||
TransmitListener tx;
|
||||
hw.configure(true, true);
|
||||
hw.radio.register_listener(&rx);
|
||||
const auto configured_params = hw.packet_params;
|
||||
hw.dio.internal = true;
|
||||
hw.radio.call();
|
||||
ASSERT_TRUE(hw.radio.is_idle());
|
||||
hw.clear_trace();
|
||||
hw.fail_at = 3;
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&tx, hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
|
||||
EXPECT_TRUE(tx.results.empty());
|
||||
EXPECT_TRUE(hw.radio.is_in_loop_state());
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
EXPECT_EQ(hw.packet_params, configured_params);
|
||||
hw.clear_trace();
|
||||
hw.irq = IRQ_RX_DONE; // No simulated ISR edge: restoration must re-enable polling.
|
||||
hw.radio.call();
|
||||
ASSERT_EQ(rx.packets.size(), 1u);
|
||||
EXPECT_EQ(rx.packets[0], hw.packet);
|
||||
EXPECT_EQ(hw.sends, 0u);
|
||||
}
|
||||
|
||||
TEST_P(SX126xAsyncModes, ReceiveAtStandbyRejectsSendAndSurvivesReadFailure) {
|
||||
const auto [lora, rx, fixed] = GetParam();
|
||||
RadioHardware hw;
|
||||
PacketListener received;
|
||||
TransmitListener tx;
|
||||
hw.configure(lora, rx, fixed);
|
||||
hw.radio.call(); // Advance from setup into ESPHome loop scheduling.
|
||||
hw.radio.register_listener(&received);
|
||||
hw.clear_trace();
|
||||
auto write = hw.cs.on_write;
|
||||
bool inject = true;
|
||||
hw.cs.on_write = [&](bool high) {
|
||||
write(high);
|
||||
if (high && inject && !hw.transactions.back().empty() && hw.transactions.back()[0] == RADIO_SET_STANDBY) {
|
||||
inject = false;
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
}
|
||||
};
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&tx, hw.packet), SX126xError::SX126X_ERROR_BUSY);
|
||||
EXPECT_TRUE(tx.results.empty());
|
||||
EXPECT_TRUE(received.packets.empty());
|
||||
EXPECT_FALSE(hw.receiving);
|
||||
EXPECT_FALSE(hw.radio.is_transmitting());
|
||||
EXPECT_EQ(hw.irq, IRQ_RX_DONE);
|
||||
ASSERT_EQ(hw.transactions.size(), 2u);
|
||||
EXPECT_EQ(hw.transactions[0][0], RADIO_SET_STANDBY);
|
||||
EXPECT_EQ(hw.transactions[1][0], RADIO_GET_IRQSTATUS);
|
||||
const auto resets = hw.rst.writes;
|
||||
hw.radio.set_mode_rx();
|
||||
EXPECT_EQ(hw.rst.writes, resets);
|
||||
EXPECT_EQ(hw.transactions.size(), 2u);
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&tx, hw.packet), SX126xError::SX126X_ERROR_BUSY);
|
||||
EXPECT_FALSE(hw.radio.cancel_transmit_async(&tx));
|
||||
|
||||
hw.clear_trace();
|
||||
hw.fail_at = 2; // Packet metadata read fails after the held IRQ is read.
|
||||
hw.radio.loop();
|
||||
EXPECT_TRUE(hw.radio.is_in_loop_state());
|
||||
EXPECT_TRUE(received.packets.empty());
|
||||
EXPECT_EQ(hw.irq, IRQ_RX_DONE);
|
||||
EXPECT_FALSE(hw.receiving);
|
||||
hw.clear_trace();
|
||||
LoopBlockingGuard retry{nullptr, nullptr, App.get_loop_component_start_time() + 200};
|
||||
hw.radio.loop();
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(received.packets.size(), 1u);
|
||||
EXPECT_EQ(received.packets[0], hw.packet);
|
||||
EXPECT_EQ(hw.receiving, rx);
|
||||
EXPECT_EQ(hw.sends, 0u);
|
||||
EXPECT_TRUE(tx.results.empty());
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&tx, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(tx.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
}
|
||||
|
||||
TEST_P(SX126xAsyncModes, FailedPreparationAndRestorationRequireSuccessfulConfiguration) {
|
||||
const auto [lora, rx, fixed] = GetParam();
|
||||
RadioHardware hw;
|
||||
TransmitListener listener;
|
||||
hw.configure(lora, rx, fixed);
|
||||
hw.clear_trace();
|
||||
const auto busy = hw.busy.on_read;
|
||||
hw.busy.on_read = [&]() { return hw.transactions.size() >= 3 || busy(); };
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
|
||||
EXPECT_TRUE(listener.results.empty());
|
||||
EXPECT_EQ(hw.sends, 0u);
|
||||
EXPECT_FALSE(hw.radio.is_transmitting());
|
||||
EXPECT_TRUE(hw.cs.level);
|
||||
ASSERT_EQ(hw.transactions.size(), 4u);
|
||||
EXPECT_TRUE(hw.transactions[2].empty()); // Failed preparation.
|
||||
EXPECT_TRUE(hw.transactions[3].empty()); // Failed restoration.
|
||||
hw.clear_trace();
|
||||
hw.busy.on_read = busy;
|
||||
hw.radio.set_mode_rx();
|
||||
hw.radio.loop();
|
||||
EXPECT_TRUE(hw.radio.is_idle());
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
hw.fail_at = 1;
|
||||
hw.radio.configure();
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
hw.clear_trace();
|
||||
hw.radio.configure();
|
||||
EXPECT_TRUE(hw.radio.is_in_loop_state());
|
||||
EXPECT_EQ(hw.receiving, rx);
|
||||
EXPECT_TRUE(listener.results.empty());
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
}
|
||||
|
||||
TEST(SX126xAsync, ExplicitConfigurationCanRecoverAnUnreadableHeldPacket) {
|
||||
RadioHardware hw;
|
||||
TransmitListener listener;
|
||||
hw.configure(true, true);
|
||||
auto write = hw.cs.on_write;
|
||||
hw.cs.on_write = [&](bool high) {
|
||||
write(high);
|
||||
if (high && !hw.transactions.back().empty() && hw.transactions.back()[0] == RADIO_SET_STANDBY) {
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
}
|
||||
};
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_BUSY);
|
||||
hw.cs.on_write = write;
|
||||
hw.busy.on_read = []() { return true; };
|
||||
hw.radio.loop();
|
||||
const auto resets = hw.rst.writes;
|
||||
hw.radio.configure();
|
||||
EXPECT_GT(hw.rst.writes, resets);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
hw.busy.on_read = []() { return false; };
|
||||
hw.radio.configure();
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
EXPECT_TRUE(listener.results.empty());
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
}
|
||||
|
||||
class SX126xReceiveActions : public ::testing::TestWithParam<unsigned> {};
|
||||
|
||||
TEST_P(SX126xReceiveActions, ModeAndConfigurationPreserveBorrowedPacket) {
|
||||
RadioHardware hw;
|
||||
ReceiveCallback action;
|
||||
PacketListener observer;
|
||||
hw.configure(true, true);
|
||||
hw.radio.register_listener(&action);
|
||||
hw.radio.register_listener(&observer);
|
||||
const auto original = hw.packet;
|
||||
const unsigned selected_action = GetParam();
|
||||
unsigned callbacks = 0;
|
||||
action.on_packet_callback = [&]() {
|
||||
callbacks++;
|
||||
hw.clear_trace();
|
||||
switch (selected_action) {
|
||||
case 0:
|
||||
hw.radio.set_mode_sleep();
|
||||
break;
|
||||
case 1:
|
||||
hw.radio.set_mode_sleep(true);
|
||||
break;
|
||||
case 2:
|
||||
hw.radio.set_rx_start(false);
|
||||
hw.radio.configure();
|
||||
break;
|
||||
case 3:
|
||||
hw.radio.set_mode_standby(STDBY_XOSC);
|
||||
break;
|
||||
case 4:
|
||||
hw.radio.set_mode_rx();
|
||||
break;
|
||||
case 5:
|
||||
hw.radio.run_image_cal();
|
||||
break;
|
||||
default:
|
||||
FAIL() << "Unknown action " << selected_action;
|
||||
}
|
||||
EXPECT_FALSE(hw.transactions.empty()); // The requested operation must actually reach the chip.
|
||||
EXPECT_EQ(hw.receiving, selected_action >= 4);
|
||||
const auto commands = hw.transactions.size();
|
||||
hw.packet.assign(19, 0x99);
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
hw.radio.loop(); // Recursive packet delivery is still forbidden, including after configure().
|
||||
EXPECT_EQ(hw.transactions.size(), commands);
|
||||
};
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(callbacks, 1u);
|
||||
ASSERT_EQ(observer.packets.size(), 1u);
|
||||
EXPECT_EQ(observer.packets.front(), original);
|
||||
EXPECT_EQ(hw.irq, IRQ_RX_DONE); // The next packet is left for a later loop.
|
||||
if (selected_action == 1) {
|
||||
TransmitListener tx;
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&tx, original), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(SleepColdSleepConfigureStandbyRxAndCalibration, SX126xReceiveActions,
|
||||
::testing::Range(0u, 6u));
|
||||
|
||||
} // namespace esphome::sx126x::testing
|
||||
@@ -0,0 +1,788 @@
|
||||
#include "sx126x_test_helpers.h"
|
||||
#include <cmath>
|
||||
#include "esphome/core/application.h"
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#ifdef USE_HOST
|
||||
#include <string>
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
namespace esphome::sx126x::testing {
|
||||
class SX126xBusySetup : public ::testing::TestWithParam<std::tuple<bool, bool>> {};
|
||||
|
||||
TEST_P(SX126xBusySetup, StopsAtEveryConfigurationCommand) {
|
||||
const auto [lora, rx] = GetParam();
|
||||
RadioHardware reference;
|
||||
reference.configure(lora, rx);
|
||||
ASSERT_FALSE(reference.radio.is_failed());
|
||||
ASSERT_FALSE(reference.radio.status_has_warning());
|
||||
const size_t count = reference.transactions.size();
|
||||
ASSERT_GT(count, 10u);
|
||||
for (size_t fault = 1; fault <= count; fault++) {
|
||||
SCOPED_TRACE(fault);
|
||||
RadioHardware hardware;
|
||||
hardware.fail_at = fault;
|
||||
hardware.configure(lora, rx);
|
||||
hardware.expect_aborted(fault);
|
||||
EXPECT_TRUE(hardware.radio.is_failed());
|
||||
hardware.radio.dump_config(); // Also safe if the version read never happened.
|
||||
hardware.clear_trace();
|
||||
hardware.radio.configure();
|
||||
hardware.radio.set_mode_rx();
|
||||
hardware.radio.set_mode_tx();
|
||||
hardware.radio.set_mode_sleep();
|
||||
hardware.radio.set_mode_standby(STDBY_XOSC);
|
||||
hardware.radio.run_image_cal();
|
||||
hardware.radio.loop();
|
||||
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
EXPECT_TRUE(hardware.transactions.empty());
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(LoRaAndFSKWithRxOrSleep, SX126xBusySetup,
|
||||
::testing::Combine(::testing::Bool(), ::testing::Bool()));
|
||||
|
||||
class SX126xBusyTransmit : public ::testing::TestWithParam<std::tuple<bool, bool, bool>> {};
|
||||
|
||||
TEST_P(SX126xBusyTransmit, StopsAtEverySendAndCleanupCommandAndCanRetry) {
|
||||
const auto [lora, rx, fixed] = GetParam();
|
||||
RadioHardware reference;
|
||||
reference.configure(lora, rx, fixed);
|
||||
reference.clear_trace();
|
||||
ASSERT_EQ(reference.radio.transmit_packet(reference.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
const size_t count = reference.transactions.size();
|
||||
for (size_t fault = 1; fault <= count; fault++) {
|
||||
SCOPED_TRACE(fault);
|
||||
RadioHardware hardware;
|
||||
hardware.configure(lora, rx, fixed);
|
||||
hardware.clear_trace();
|
||||
hardware.fail_at = fault;
|
||||
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet.data(), hardware.packet.size()),
|
||||
SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
|
||||
EXPECT_TRUE(hardware.transactions[fault - 1].empty());
|
||||
EXPECT_TRUE(hardware.cs.level);
|
||||
EXPECT_FALSE(hardware.radio.is_failed());
|
||||
hardware.clear_trace();
|
||||
hardware.radio.configure();
|
||||
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_FALSE(hardware.radio.status_has_warning());
|
||||
EXPECT_EQ(hardware.receiving, rx);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(SX126xBusyTransmit, RestoresEveryPreparationFailureWithoutAnotherTransmission) {
|
||||
const auto [lora, rx, fixed] = GetParam();
|
||||
RadioHardware reference;
|
||||
reference.configure(lora, rx, fixed);
|
||||
reference.clear_trace();
|
||||
ASSERT_EQ(reference.radio.transmit_packet(reference.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
size_t tx_command = 0;
|
||||
for (size_t i = 0; i < reference.transactions.size(); i++) {
|
||||
if (reference.transactions[i][0] == RADIO_SET_TX) {
|
||||
tx_command = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_NE(tx_command, 0u);
|
||||
for (size_t fault = 1; fault <= tx_command; fault++) {
|
||||
SCOPED_TRACE(fault);
|
||||
RadioHardware hw;
|
||||
PacketListener listener;
|
||||
hw.configure(lora, rx, fixed);
|
||||
hw.radio.register_listener(&listener);
|
||||
const auto configured_params = hw.packet_params;
|
||||
hw.clear_trace();
|
||||
hw.fail_at = fault;
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
|
||||
EXPECT_TRUE(hw.transactions[fault - 1].empty());
|
||||
EXPECT_TRUE(hw.cs.level);
|
||||
EXPECT_EQ(hw.ends, hw.transactions.size());
|
||||
EXPECT_EQ(hw.receiving, rx);
|
||||
EXPECT_EQ(hw.packet_params, configured_params);
|
||||
EXPECT_EQ(hw.irq, 0u);
|
||||
EXPECT_FALSE(hw.radio.status_has_warning());
|
||||
EXPECT_FALSE(hw.radio.is_failed());
|
||||
const auto sends = hw.sends;
|
||||
hw.clear_trace();
|
||||
if (rx) {
|
||||
if (!fixed) {
|
||||
hw.packet.resize(9, 0x55); // RX must accept a different length than the attempted TX.
|
||||
}
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(listener.packets.size(), 1u);
|
||||
EXPECT_EQ(listener.packets[0], hw.packet);
|
||||
}
|
||||
EXPECT_EQ(hw.sends, sends);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(SX126xBusyTransmit, FailedRestorationRequiresConfigureBeforeReceiveRecovery) {
|
||||
const auto [lora, rx, fixed] = GetParam();
|
||||
RadioHardware reference;
|
||||
reference.configure(lora, rx, fixed);
|
||||
reference.clear_trace();
|
||||
ASSERT_EQ(reference.radio.transmit_packet(reference.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
size_t restoration = 0;
|
||||
bool sent = false;
|
||||
for (size_t i = 0; i < reference.transactions.size(); i++) {
|
||||
if (reference.transactions[i][0] == RADIO_SET_TX) {
|
||||
sent = true;
|
||||
}
|
||||
if (sent && reference.transactions[i][0] == RADIO_SET_STANDBY) {
|
||||
restoration = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_NE(restoration, 0u);
|
||||
for (size_t fault = restoration; fault <= reference.transactions.size(); fault++) {
|
||||
SCOPED_TRACE(fault);
|
||||
RadioHardware hw;
|
||||
PacketListener listener;
|
||||
hw.configure(lora, rx, fixed);
|
||||
hw.radio.register_listener(&listener);
|
||||
const auto configured_params = hw.packet_params;
|
||||
hw.clear_trace();
|
||||
hw.fail_at = fault;
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
|
||||
EXPECT_TRUE(hw.cs.level);
|
||||
EXPECT_TRUE(hw.radio.status_has_warning());
|
||||
EXPECT_FALSE(hw.radio.is_failed());
|
||||
const auto sends = hw.sends;
|
||||
hw.clear_trace(); // Clearing BUSY alone must not re-admit partial configuration.
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
hw.radio.loop();
|
||||
hw.radio.set_mode_rx();
|
||||
hw.radio.set_mode_tx();
|
||||
hw.radio.set_mode_standby(STDBY_XOSC);
|
||||
hw.radio.set_mode_sleep();
|
||||
hw.radio.run_image_cal();
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
EXPECT_TRUE(listener.packets.empty());
|
||||
EXPECT_TRUE(hw.radio.status_has_warning());
|
||||
hw.irq = 0; // Hardware reset clears IRQs; the GPIO fake does not model reset.
|
||||
hw.radio.configure();
|
||||
EXPECT_FALSE(hw.radio.status_has_warning());
|
||||
EXPECT_EQ(hw.packet_params, configured_params);
|
||||
EXPECT_EQ(hw.receiving, rx);
|
||||
if (rx) {
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(listener.packets.size(), 1u);
|
||||
EXPECT_EQ(listener.packets[0], hw.packet);
|
||||
}
|
||||
EXPECT_EQ(hw.sends, sends); // Recovery must not depend on sending another packet.
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, PersistentBusyDuringPreparationAndRestorationRequiresConfigure) {
|
||||
RadioHardware hw;
|
||||
PacketListener listener;
|
||||
hw.configure(true, true);
|
||||
hw.radio.register_listener(&listener);
|
||||
const auto configured_params = hw.packet_params;
|
||||
hw.clear_trace();
|
||||
auto normal_busy = hw.busy.on_read;
|
||||
hw.busy.on_read = [&]() { return hw.transactions.size() >= 3 || normal_busy(); };
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
|
||||
EXPECT_FALSE(hw.receiving);
|
||||
EXPECT_TRUE(hw.radio.status_has_warning());
|
||||
EXPECT_EQ(hw.sends, 0u);
|
||||
hw.busy.on_read = normal_busy;
|
||||
hw.clear_trace();
|
||||
hw.radio.set_mode_rx();
|
||||
hw.radio.run_image_cal();
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
EXPECT_TRUE(hw.radio.status_has_warning());
|
||||
hw.radio.configure();
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
EXPECT_EQ(hw.packet_params, configured_params);
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(listener.packets.size(), 1u);
|
||||
EXPECT_EQ(listener.packets[0], hw.packet);
|
||||
EXPECT_EQ(hw.sends, 0u);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(LoRaAndFSKWithRxOrSleepAndFixedOrVariablePayload, SX126xBusyTransmit,
|
||||
::testing::Combine(::testing::Bool(), ::testing::Bool(), ::testing::Bool()));
|
||||
|
||||
class SX126xBusyReceive : public ::testing::TestWithParam<bool> {};
|
||||
|
||||
TEST_P(SX126xBusyReceive, KeepsAPacketWhoseReadFailedAndRetries) {
|
||||
const bool lora = GetParam();
|
||||
for (size_t fault = 1; fault <= 5; fault++) {
|
||||
SCOPED_TRACE(fault);
|
||||
RadioHardware hardware;
|
||||
PacketListener listener;
|
||||
hardware.configure(lora, true);
|
||||
hardware.radio.register_listener(&listener);
|
||||
hardware.clear_trace();
|
||||
hardware.irq = IRQ_RX_DONE;
|
||||
hardware.fail_at = fault;
|
||||
hardware.radio.loop();
|
||||
hardware.expect_aborted(fault);
|
||||
EXPECT_TRUE(listener.packets.empty());
|
||||
EXPECT_TRUE(hardware.radio.status_has_warning());
|
||||
EXPECT_FALSE(hardware.radio.is_failed());
|
||||
// The flags may already be cleared; the packet is still read on the retry.
|
||||
hardware.clear_trace();
|
||||
LoopBlockingGuard retry{nullptr, nullptr, App.get_loop_component_start_time() + 200};
|
||||
hardware.radio.loop();
|
||||
ASSERT_EQ(listener.packets.size(), 1u);
|
||||
EXPECT_EQ(listener.packets[0], hardware.packet);
|
||||
EXPECT_FLOAT_EQ(listener.rssi, lora ? -40 : -42);
|
||||
if (lora) {
|
||||
EXPECT_FLOAT_EQ(listener.snr, 10);
|
||||
} else {
|
||||
EXPECT_TRUE(std::isnan(listener.snr));
|
||||
}
|
||||
EXPECT_FALSE(hardware.radio.status_has_warning());
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(LoRaAndFSK, SX126xBusyReceive, ::testing::Bool());
|
||||
|
||||
// A packet whose flags were cleared before its read failed, then held by a send, survives a
|
||||
// second failed read
|
||||
TEST(SX126xBusy, HeldPacketSurvivesASecondFailedRead) {
|
||||
RadioHardware hw;
|
||||
PacketListener listener;
|
||||
hw.configure(true, true);
|
||||
hw.radio.register_listener(&listener);
|
||||
hw.clear_trace();
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
hw.fail_at = 3; // the first read after the flags were cleared
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(hw.irq, 0);
|
||||
ASSERT_TRUE(listener.packets.empty());
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY);
|
||||
hw.clear_trace();
|
||||
hw.fail_at = 2; // the held packet's read
|
||||
LoopBlockingGuard first{nullptr, nullptr, App.get_loop_component_start_time() + 200};
|
||||
hw.radio.loop();
|
||||
ASSERT_TRUE(listener.packets.empty());
|
||||
hw.clear_trace();
|
||||
LoopBlockingGuard second{nullptr, nullptr, App.get_loop_component_start_time() + 200};
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(listener.packets.size(), 1u);
|
||||
EXPECT_EQ(listener.packets[0], hw.packet);
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, KeepsPollingAnUnclearedInterruptAfterBusyTimeout) {
|
||||
RadioHardware hardware;
|
||||
PacketListener listener;
|
||||
hardware.configure(true, true);
|
||||
hardware.radio.register_listener(&listener);
|
||||
hardware.dio.internal = true; // No new ISR edge while the pending IRQ stays high.
|
||||
hardware.clear_trace();
|
||||
hardware.fail_at = 1;
|
||||
hardware.irq = IRQ_RX_DONE;
|
||||
hardware.radio.call();
|
||||
EXPECT_TRUE(listener.packets.empty());
|
||||
hardware.clear_trace();
|
||||
LoopBlockingGuard retry{nullptr, nullptr, App.get_loop_component_start_time() + 200};
|
||||
hardware.radio.call();
|
||||
EXPECT_EQ(listener.packets.size(), 1u);
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, AssertsChipSelectBeforeWaitingForWakeup) {
|
||||
RadioHardware hardware;
|
||||
hardware.configure(true, true);
|
||||
hardware.clear_trace();
|
||||
hardware.busy.level = true; // The CS callback releases BUSY only after selecting the chip.
|
||||
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_TRUE(hardware.cs.level);
|
||||
EXPECT_EQ(hardware.ends, hardware.transactions.size());
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, RejectsSendBeforeSetupWithoutTouchingPins) {
|
||||
RadioHardware hardware;
|
||||
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
EXPECT_TRUE(hardware.transactions.empty());
|
||||
EXPECT_EQ(hardware.busy.reads, 0u);
|
||||
EXPECT_EQ(hardware.dio.reads, 0u);
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, UnsupportedVersionFailsSetup) {
|
||||
RadioHardware hardware;
|
||||
hardware.supported_version = false;
|
||||
hardware.configure(true, true);
|
||||
EXPECT_TRUE(hardware.radio.is_failed());
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, RuntimeVersionFailureRequiresConfigureWithoutPermanentlyFailing) {
|
||||
RadioHardware hardware;
|
||||
hardware.configure(true, true);
|
||||
hardware.supported_version = false;
|
||||
hardware.radio.configure();
|
||||
EXPECT_FALSE(hardware.radio.is_failed());
|
||||
EXPECT_TRUE(hardware.radio.status_has_warning());
|
||||
hardware.clear_trace();
|
||||
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
EXPECT_TRUE(hardware.transactions.empty());
|
||||
hardware.supported_version = true;
|
||||
hardware.radio.configure();
|
||||
EXPECT_FALSE(hardware.radio.status_has_warning());
|
||||
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, FailedReconfigurationRequiresSuccessfulConfigureBeforeOperations) {
|
||||
RadioHardware reference;
|
||||
reference.configure(true, true);
|
||||
const size_t count = reference.transactions.size();
|
||||
for (size_t fault = 1; fault <= count; fault++) {
|
||||
SCOPED_TRACE(fault);
|
||||
RadioHardware hw;
|
||||
PacketListener listener;
|
||||
hw.configure(true, true);
|
||||
hw.radio.register_listener(&listener);
|
||||
hw.clear_trace();
|
||||
hw.fail_at = fault;
|
||||
hw.radio.configure();
|
||||
EXPECT_FALSE(hw.radio.is_failed());
|
||||
hw.clear_trace();
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
hw.radio.loop();
|
||||
hw.radio.set_mode_rx();
|
||||
hw.radio.set_mode_tx();
|
||||
hw.radio.set_mode_standby(STDBY_XOSC);
|
||||
hw.radio.set_mode_sleep();
|
||||
hw.radio.run_image_cal();
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
EXPECT_TRUE(listener.packets.empty());
|
||||
hw.radio.configure();
|
||||
hw.irq = 0;
|
||||
EXPECT_FALSE(hw.radio.status_has_warning());
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, RetryClearsStaleTxDoneBeforeWaitingForDelayedCompletion) {
|
||||
RadioHardware reference;
|
||||
reference.configure(true, true);
|
||||
reference.clear_trace();
|
||||
reference.radio.transmit_packet(reference.packet);
|
||||
size_t cleanup = 0;
|
||||
bool sent = false;
|
||||
for (size_t i = 0; i < reference.transactions.size(); i++) {
|
||||
if (reference.transactions[i][0] == RADIO_SET_TX) {
|
||||
sent = true;
|
||||
}
|
||||
if (sent && reference.transactions[i][0] == RADIO_CLR_IRQSTATUS) {
|
||||
cleanup = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_NE(cleanup, 0u);
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
hw.clear_trace();
|
||||
hw.fail_at = cleanup;
|
||||
ASSERT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
|
||||
EXPECT_EQ(hw.irq & IRQ_TX_DONE, 0); // Shared restoration retries the failed IRQ clear.
|
||||
hw.irq = IRQ_TX_DONE; // A stale flag must still not finish the next TX.
|
||||
hw.clear_trace();
|
||||
hw.auto_complete = false;
|
||||
unsigned polls = 0;
|
||||
hw.dio.on_read = [&]() {
|
||||
if (++polls == 5) {
|
||||
hw.irq |= IRQ_TX_DONE;
|
||||
}
|
||||
return hw.irq != 0;
|
||||
};
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_EQ(polls, 5u);
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
}
|
||||
|
||||
} // namespace esphome::sx126x::testing
|
||||
|
||||
namespace esphome::sx126x::testing {
|
||||
template<typename T>
|
||||
concept RawSpiAccessible = requires(T &device) { device.transfer_byte(0); };
|
||||
template<typename T>
|
||||
concept ManualSelectionAccessible = requires(T &device) {
|
||||
device.enable();
|
||||
device.disable();
|
||||
};
|
||||
static_assert(!RawSpiAccessible<SX126x>);
|
||||
static_assert(!RawSpiAccessible<SX126xSpiDevice>);
|
||||
static_assert(!ManualSelectionAccessible<SX126x>);
|
||||
static_assert(!ManualSelectionAccessible<SX126xSpiDevice>);
|
||||
// The radio runs every command sequence and state change. Without component status or
|
||||
// listeners it cannot run user code mid-operation; SX126x::apply_() does that afterwards.
|
||||
static_assert(!std::is_base_of_v<Component, SX126xRadio>);
|
||||
} // namespace esphome::sx126x::testing
|
||||
|
||||
namespace esphome::sx126x::testing {
|
||||
TEST(SX126xBusy, FailedModeChangeCannotBeClearedByAnotherModeCommand) {
|
||||
for (size_t fault = 1; fault <= 3; fault++) {
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
hw.clear_trace();
|
||||
hw.fail_at = fault;
|
||||
hw.radio.set_mode_rx();
|
||||
EXPECT_TRUE(hw.radio.status_has_warning());
|
||||
hw.clear_trace();
|
||||
hw.radio.set_mode_standby(STDBY_XOSC);
|
||||
hw.radio.run_image_cal();
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
EXPECT_TRUE(hw.radio.status_has_warning());
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
hw.radio.configure();
|
||||
EXPECT_FALSE(hw.radio.status_has_warning());
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, ConfigurationCannotReenterSendPreparationOrRestoration) {
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
hw.radio.call(); // Advance from setup into ESPHome loop scheduling.
|
||||
hw.clear_trace();
|
||||
auto write = hw.cs.on_write;
|
||||
unsigned guarded = 0;
|
||||
hw.cs.on_write = [&](bool high) {
|
||||
write(high);
|
||||
if (!high || hw.transactions.back().empty() || hw.transactions.back()[0] != RADIO_SET_STANDBY) {
|
||||
return;
|
||||
}
|
||||
guarded++;
|
||||
const auto commands = hw.transactions.size();
|
||||
hw.radio.configure();
|
||||
hw.radio.set_mode_rx();
|
||||
hw.radio.loop();
|
||||
EXPECT_TRUE(hw.radio.is_in_loop_state());
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY);
|
||||
EXPECT_EQ(hw.transactions.size(), commands);
|
||||
};
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_EQ(guarded, 2u);
|
||||
EXPECT_TRUE(hw.radio.is_in_loop_state());
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
}
|
||||
} // namespace esphome::sx126x::testing
|
||||
|
||||
namespace esphome::sx126x::testing {
|
||||
TEST(SX126xBusy, InvalidPayloadDoesNotEnterAnOperation) {
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
hw.clear_trace();
|
||||
EXPECT_EQ(hw.radio.transmit_packet(nullptr, 3), SX126xError::SX126X_ERROR_INVALID_PARAMS);
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet.data(), 0), SX126xError::SX126X_ERROR_INVALID_PARAMS);
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, ColdSleepIsWokenByAModeCommandAndWarmSleepRetainsIt) {
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
hw.radio.set_mode_sleep(false);
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.radio.set_mode_sleep(true);
|
||||
hw.clear_trace();
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
// Sleeping again leaves it asleep
|
||||
hw.radio.set_mode_sleep(false);
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
// Any other mode command applies the lost configuration first
|
||||
hw.radio.set_mode_rx();
|
||||
EXPECT_FALSE(hw.transactions.empty());
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
}
|
||||
|
||||
// Runs the scheduler for ms milliseconds of real time, as the main loop would
|
||||
static void run_scheduler(uint32_t ms) {
|
||||
const uint32_t start = millis();
|
||||
while (millis() - start < ms) {
|
||||
App.scheduler.call(millis());
|
||||
delay(5);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, FailedModeChangeIsReconfiguredWithoutBeingAsked) {
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
hw.clear_trace();
|
||||
hw.fail_at = 1;
|
||||
hw.radio.set_mode_rx();
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
run_scheduler(1100);
|
||||
EXPECT_FALSE(hw.radio.status_has_warning());
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
// Once configured, the retry stops
|
||||
hw.clear_trace();
|
||||
run_scheduler(1100);
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, ColdSleepIsNotReconfiguredWithoutBeingAsked) {
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
hw.radio.set_mode_sleep(true);
|
||||
hw.clear_trace();
|
||||
run_scheduler(1100);
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, ConfigureBeforeSetupDoesNothing) {
|
||||
RadioHardware hw;
|
||||
hw.radio.configure();
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
EXPECT_EQ(hw.rst.writes, 0u);
|
||||
hw.configure(true, true);
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
}
|
||||
|
||||
// The TX mode command sends whatever the FIFO holds without tracking it; its TX_DONE is
|
||||
// cleared as a flag without a packet, and a send still works afterwards
|
||||
TEST(SX126xBusy, RawTxModeLeavesTheRadioUsable) {
|
||||
RadioHardware hw;
|
||||
PacketListener listener;
|
||||
hw.configure(true, true);
|
||||
hw.radio.register_listener(&listener);
|
||||
hw.radio.set_mode_tx();
|
||||
ASSERT_EQ(hw.irq & IRQ_TX_DONE, IRQ_TX_DONE);
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(hw.irq, 0);
|
||||
EXPECT_TRUE(listener.packets.empty());
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, PacketThatFailedItsCrcDoesNotHoldUpASend) {
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
hw.irq = IRQ_RX_DONE | IRQ_CRC_ERROR;
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, PacketCompletingDuringAReadIsReadNext) {
|
||||
RadioHardware hw;
|
||||
PacketListener listener;
|
||||
hw.configure(true, true);
|
||||
hw.radio.register_listener(&listener);
|
||||
bool second = false;
|
||||
hw.on_command = [&](uint8_t opcode) {
|
||||
if (opcode == RADIO_READ_BUFFER && !second) {
|
||||
second = true;
|
||||
hw.irq |= IRQ_RX_DONE;
|
||||
}
|
||||
};
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(listener.packets.size(), 1u);
|
||||
EXPECT_EQ(hw.irq & IRQ_RX_DONE, IRQ_RX_DONE); // DIO1 stays high for the second packet
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(listener.packets.size(), 2u);
|
||||
}
|
||||
|
||||
TEST(SX126xBusy, UnavailableLoopStopsAndConfigurationResumesReceiveWithoutAnInterrupt) {
|
||||
for (bool internal : {false, true}) {
|
||||
for (unsigned cause = 0; cause < 3; cause++) {
|
||||
SCOPED_TRACE(internal);
|
||||
SCOPED_TRACE(cause);
|
||||
RadioHardware hw;
|
||||
PacketListener received;
|
||||
hw.configure(true, true);
|
||||
hw.radio.register_listener(&received);
|
||||
// Set after setup: the fake pin cannot attach a hardware interrupt.
|
||||
hw.dio.internal = internal;
|
||||
hw.clear_trace();
|
||||
if (cause == 0) {
|
||||
hw.radio.set_mode_sleep(true);
|
||||
} else if (cause == 1) {
|
||||
hw.fail_at = 1;
|
||||
hw.radio.configure();
|
||||
} else {
|
||||
hw.fail_at = 1;
|
||||
hw.radio.set_mode_rx();
|
||||
}
|
||||
hw.clear_trace();
|
||||
hw.radio.call();
|
||||
ASSERT_TRUE(hw.radio.is_idle());
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
EXPECT_EQ(hw.dio.reads, 0u);
|
||||
hw.radio.configure();
|
||||
ASSERT_TRUE(hw.radio.is_in_loop_state());
|
||||
hw.irq = IRQ_RX_DONE; // No ISR edge: configure() must resume polling itself.
|
||||
hw.radio.call();
|
||||
ASSERT_EQ(received.packets.size(), 1u);
|
||||
EXPECT_EQ(received.packets[0], hw.packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace esphome::sx126x::testing
|
||||
|
||||
namespace esphome::sx126x::testing {
|
||||
TEST(SX126xProfile, SettersDoNotChangeAppliedPacketOrRestorationPolicy) {
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
hw.radio.set_payload_length(2);
|
||||
hw.radio.set_rx_start(false);
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
hw.radio.configure();
|
||||
EXPECT_EQ(hw.radio.get_max_packet_size(), 2u);
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_INVALID_PARAMS);
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet.data(), 2), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_FALSE(hw.receiving);
|
||||
}
|
||||
|
||||
TEST(SX126xProfile, RejectsOversizedSyncValueBeforeResetOrSpi) {
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
hw.radio.set_sync_value(std::vector<uint8_t>(9, 1));
|
||||
hw.clear_trace();
|
||||
const auto reset_writes = hw.rst.writes;
|
||||
hw.radio.configure();
|
||||
EXPECT_EQ(hw.rst.writes, reset_writes);
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
hw.radio.set_sync_value({0x14, 0x24});
|
||||
hw.radio.configure();
|
||||
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
}
|
||||
} // namespace esphome::sx126x::testing
|
||||
|
||||
#ifdef USE_HOST
|
||||
namespace esphome::sx126x::testing {
|
||||
template<typename F> static std::string capture_radio_logs(F action) {
|
||||
const auto previous_baud_rate = logger::global_logger->get_baud_rate();
|
||||
::testing::internal::CaptureStdout();
|
||||
logger::global_logger->set_baud_rate(115200);
|
||||
action();
|
||||
logger::global_logger->set_baud_rate(previous_baud_rate);
|
||||
return ::testing::internal::GetCapturedStdout();
|
||||
}
|
||||
|
||||
TEST(SX126xDiagnostics, BlockingRejectionsExplainWhyNoPacketWasSent) {
|
||||
RadioHardware hw;
|
||||
auto output = capture_radio_logs(
|
||||
[&]() { EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY); });
|
||||
EXPECT_NE(output.find("Packet send failed: radio not ready"), std::string::npos);
|
||||
hw.configure(true, true, true);
|
||||
hw.clear_trace();
|
||||
output = capture_radio_logs(
|
||||
[&]() { EXPECT_EQ(hw.radio.transmit_packet(hw.packet.data(), 2), SX126xError::SX126X_ERROR_INVALID_PARAMS); });
|
||||
EXPECT_NE(output.find("Packet send failed: invalid payload"), std::string::npos);
|
||||
EXPECT_TRUE(hw.transactions.empty());
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
output =
|
||||
capture_radio_logs([&]() { EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY); });
|
||||
EXPECT_NE(output.find("Packet send failed: radio busy"), std::string::npos);
|
||||
EXPECT_EQ(hw.irq, IRQ_RX_DONE);
|
||||
EXPECT_EQ(hw.sends, 0u);
|
||||
hw.radio.loop(); // Deliver the held packet, which also clears its IRQ.
|
||||
EXPECT_EQ(hw.irq, 0u);
|
||||
hw.clear_trace();
|
||||
hw.fail_at = 1;
|
||||
output = capture_radio_logs(
|
||||
[&]() { EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT); });
|
||||
EXPECT_NE(output.find("Packet send failed: bus timeout"), std::string::npos);
|
||||
EXPECT_EQ(hw.sends, 0u);
|
||||
}
|
||||
|
||||
TEST(SX126xDiagnostics, AsyncAdmissionLeavesRejectionReportingToItsCaller) {
|
||||
class Completion final : public SX126xTransmitListener {
|
||||
public:
|
||||
void on_transmit_complete(SX126xError result) override { ADD_FAILURE() << "Rejected send completed"; }
|
||||
} completion;
|
||||
RadioHardware hw;
|
||||
const auto output = capture_radio_logs([&]() {
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&completion, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
hw.configure(true, true, true);
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&completion, hw.packet.data(), 2),
|
||||
SX126xError::SX126X_ERROR_INVALID_PARAMS);
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&completion, hw.packet), SX126xError::SX126X_ERROR_BUSY);
|
||||
});
|
||||
EXPECT_EQ(output.find("Packet send failed"), std::string::npos);
|
||||
EXPECT_EQ(hw.sends, 0u);
|
||||
}
|
||||
|
||||
TEST(SX126xDiagnostics, FailedSetupPrintsRequestedConfiguration) {
|
||||
RadioHardware hw;
|
||||
hw.fail_at = 1;
|
||||
hw.radio.set_pa_power(10);
|
||||
hw.configure(true, true, true);
|
||||
ASSERT_TRUE(hw.radio.is_failed());
|
||||
const auto output = capture_radio_logs([&]() { hw.radio.dump_config(); });
|
||||
EXPECT_NE(output.find("Ready for operation: NO"), std::string::npos);
|
||||
EXPECT_NE(output.find("Requested configuration:"), std::string::npos);
|
||||
EXPECT_NE(output.find("Frequency: 915000000 Hz"), std::string::npos);
|
||||
EXPECT_NE(output.find("PA Power: 10 dBm"), std::string::npos);
|
||||
EXPECT_NE(output.find("Payload Length: 3"), std::string::npos);
|
||||
EXPECT_NE(output.find("Sync Value: 0x1424"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(SX126xDiagnostics, StagedConfigurationIsLabelledAndDoesNotAlterAppliedSettings) {
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true, true);
|
||||
hw.radio.set_frequency(868000000);
|
||||
hw.radio.set_payload_length(2);
|
||||
auto output = capture_radio_logs([&]() { hw.radio.dump_config(); });
|
||||
EXPECT_NE(output.find("Ready for operation: YES"), std::string::npos);
|
||||
EXPECT_NE(output.find("Requested configuration:"), std::string::npos);
|
||||
EXPECT_NE(output.find("Frequency: 868000000 Hz"), std::string::npos);
|
||||
EXPECT_NE(output.find("Payload Length: 2"), std::string::npos);
|
||||
EXPECT_EQ(hw.radio.get_max_packet_size(), 3u);
|
||||
hw.clear_trace();
|
||||
hw.fail_at = 1;
|
||||
hw.radio.configure();
|
||||
output = capture_radio_logs([&]() { hw.radio.dump_config(); });
|
||||
EXPECT_NE(output.find("Ready for operation: NO"), std::string::npos);
|
||||
EXPECT_NE(output.find("Frequency: 868000000 Hz"), std::string::npos);
|
||||
EXPECT_EQ(hw.radio.get_max_packet_size(), 3u);
|
||||
}
|
||||
class SX126xTimeoutDiagnostics : public ::testing::TestWithParam<bool> {};
|
||||
|
||||
TEST_P(SX126xTimeoutDiagnostics, BlockingTimeoutLogsFailureAfterRestoration) {
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
hw.auto_complete = false;
|
||||
if (GetParam()) {
|
||||
hw.dio.on_read = [&]() {
|
||||
if (hw.sends > 0) {
|
||||
hw.irq = IRQ_RX_TX_TIMEOUT;
|
||||
}
|
||||
return hw.irq != 0;
|
||||
};
|
||||
}
|
||||
const auto output =
|
||||
capture_radio_logs([&]() { EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_TIMEOUT); });
|
||||
EXPECT_NE(output.find("Packet send failed: timeout"), std::string::npos);
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
EXPECT_FALSE(hw.radio.is_transmitting());
|
||||
EXPECT_FALSE(hw.radio.status_has_warning()); // Restoration itself succeeded.
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(SoftwareDeadlineAndRadioTimeout, SX126xTimeoutDiagnostics, ::testing::Bool());
|
||||
|
||||
TEST(SX126xDiagnostics, AsyncTimeoutLeavesCompletionReportingToItsCaller) {
|
||||
struct Completion final : SX126xTransmitListener {
|
||||
void on_transmit_complete(SX126xError result) override {
|
||||
this->count++;
|
||||
EXPECT_EQ(result, SX126xError::SX126X_ERROR_TIMEOUT);
|
||||
}
|
||||
unsigned count{0};
|
||||
} completion;
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
const auto output = capture_radio_logs([&]() {
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&completion, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.irq = IRQ_RX_TX_TIMEOUT;
|
||||
hw.radio.loop();
|
||||
});
|
||||
EXPECT_EQ(completion.count, 1u);
|
||||
EXPECT_EQ(output.find("Packet send failed"), std::string::npos);
|
||||
}
|
||||
|
||||
} // namespace esphome::sx126x::testing
|
||||
#endif
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "sx126x_test_helpers.h"
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
namespace esphome::sx126x::testing {
|
||||
|
||||
class RadioLogCallback {
|
||||
public:
|
||||
RadioLogCallback() : previous_(logger::global_logger) {
|
||||
this->logger_.pre_setup();
|
||||
this->logger_.set_log_level(ESPHOME_LOG_LEVEL_WARN);
|
||||
this->logger_.add_log_callback(this,
|
||||
[](void *instance, uint8_t level, const char *tag, const char *message, size_t len) {
|
||||
auto *self = static_cast<RadioLogCallback *>(instance);
|
||||
if (self->callback_) {
|
||||
self->callback_(std::string(message, len));
|
||||
}
|
||||
});
|
||||
}
|
||||
~RadioLogCallback() { logger::global_logger = this->previous_; }
|
||||
void set_callback(std::function<void(const std::string &)> callback) { this->callback_ = std::move(callback); }
|
||||
|
||||
private:
|
||||
std::function<void(const std::string &)> callback_;
|
||||
logger::Logger *previous_;
|
||||
logger::Logger logger_{0};
|
||||
};
|
||||
|
||||
struct ReentryCompletion final : SX126xTransmitListener {
|
||||
void on_transmit_complete(SX126xError result) override { this->results.push_back(result); }
|
||||
std::vector<SX126xError> results;
|
||||
};
|
||||
|
||||
// Setup reports why it failed, not a generic reason
|
||||
TEST(SX126xReentry, FailedSetupKeepsItsReason) {
|
||||
RadioLogCallback log;
|
||||
std::vector<std::string> messages;
|
||||
log.set_callback([&](const std::string &message) { messages.push_back(message); });
|
||||
RadioHardware hw;
|
||||
hw.supported_version = false;
|
||||
hw.configure(true, true);
|
||||
ASSERT_TRUE(hw.radio.is_failed());
|
||||
bool found = false;
|
||||
for (const auto &message : messages)
|
||||
found |= message.find("set Error flag: Unsupported chip version") != std::string::npos;
|
||||
EXPECT_TRUE(found);
|
||||
}
|
||||
|
||||
// Status logging is the only user code that can run inside a radio operation. It runs only
|
||||
// after the operation has committed its state, so a callback sees either a running send or a
|
||||
// released radio, and the interrupted operation touches nothing after the callback returns.
|
||||
|
||||
TEST(SX126xReentry, SuccessfulStartRunsNoUserCode) {
|
||||
RadioHardware hw;
|
||||
RadioLogCallback log;
|
||||
ReentryCompletion outer, nested;
|
||||
hw.configure(true, true);
|
||||
hw.auto_complete = false;
|
||||
hw.radio.status_set_warning(LOG_STR("Previous communication failure"));
|
||||
hw.irq = IRQ_TX_DONE; // A stale IRQ must not complete the new send either.
|
||||
unsigned callbacks = 0;
|
||||
log.set_callback([&](const std::string &message) {
|
||||
if (message.find("cleared Warning flag") != std::string::npos) {
|
||||
callbacks++;
|
||||
EXPECT_FALSE(hw.radio.is_transmitting()); // Only the completion clears the warning.
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&nested, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
}
|
||||
});
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&outer, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_EQ(callbacks, 0u);
|
||||
EXPECT_TRUE(outer.results.empty());
|
||||
LoopBlockingGuard clock{nullptr, nullptr, millis()};
|
||||
hw.radio.loop();
|
||||
EXPECT_TRUE(outer.results.empty()); // The stale IRQ was cleared before the send.
|
||||
hw.irq = IRQ_TX_DONE;
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(callbacks, 1u);
|
||||
EXPECT_EQ(outer.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
EXPECT_TRUE(nested.results.empty());
|
||||
EXPECT_TRUE(hw.radio.is_transmitting());
|
||||
EXPECT_EQ(hw.sends, 2u);
|
||||
ASSERT_TRUE(hw.radio.cancel_transmit_async(&nested));
|
||||
EXPECT_EQ(nested.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_CANCELLED}));
|
||||
EXPECT_EQ(outer.results.size(), 1u);
|
||||
}
|
||||
|
||||
class SX126xStartReentry : public ::testing::TestWithParam<bool> {};
|
||||
|
||||
TEST_P(SX126xStartReentry, WarningCallbackAfterFailedStartSeesReleasedRadio) {
|
||||
const bool cold_sleep = GetParam();
|
||||
RadioHardware hw;
|
||||
RadioLogCallback log;
|
||||
ReentryCompletion outer, nested;
|
||||
hw.configure(true, true);
|
||||
hw.auto_complete = false;
|
||||
hw.radio.status_set_warning(LOG_STR("Previous communication failure"));
|
||||
hw.clear_trace();
|
||||
hw.fail_at = 3; // Preparation fails; restoration succeeds and clears the warning.
|
||||
unsigned callbacks = 0;
|
||||
size_t commands_after_callback = 0;
|
||||
log.set_callback([&](const std::string &message) {
|
||||
if (message.find("cleared Warning flag") != std::string::npos) {
|
||||
callbacks++;
|
||||
EXPECT_FALSE(hw.radio.is_transmitting());
|
||||
EXPECT_EQ(hw.receiving, true); // Restoration finished before the callback.
|
||||
if (cold_sleep) {
|
||||
hw.radio.set_mode_sleep(true);
|
||||
} else {
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&nested, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
}
|
||||
commands_after_callback = hw.transactions.size();
|
||||
}
|
||||
});
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&outer, hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
|
||||
ASSERT_EQ(callbacks, 1u);
|
||||
EXPECT_EQ(hw.transactions.size(), commands_after_callback); // The failed start touched nothing after it.
|
||||
EXPECT_TRUE(outer.results.empty()); // A rejected start never completes.
|
||||
if (cold_sleep) {
|
||||
EXPECT_FALSE(hw.radio.is_transmitting());
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&outer, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
|
||||
EXPECT_EQ(hw.sends, 0u);
|
||||
return;
|
||||
}
|
||||
EXPECT_TRUE(hw.radio.is_transmitting());
|
||||
EXPECT_EQ(hw.sends, 1u);
|
||||
hw.irq = IRQ_TX_DONE;
|
||||
hw.radio.loop();
|
||||
EXPECT_EQ(nested.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
EXPECT_TRUE(outer.results.empty());
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(NestedSendOrColdSleep, SX126xStartReentry, ::testing::Bool());
|
||||
|
||||
class SX126xPollingReentry : public ::testing::TestWithParam<std::tuple<unsigned, bool>> {};
|
||||
|
||||
TEST_P(SX126xPollingReentry, WarningCallbackCannotCancelOrDisturbAFinishedSend) {
|
||||
const auto [failed_command, reuse_listener] = GetParam();
|
||||
RadioHardware hw;
|
||||
RadioLogCallback log;
|
||||
ReentryCompletion first, second;
|
||||
auto *replacement = reuse_listener ? &first : &second;
|
||||
hw.configure(true, true);
|
||||
hw.auto_complete = false;
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&first, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.clear_trace();
|
||||
hw.fail_at = failed_command; // 0: success, 1: IRQ read fails, 2: IRQ clear fails.
|
||||
hw.irq = IRQ_TX_DONE;
|
||||
hw.radio.status_set_warning(LOG_STR("Previous communication failure"));
|
||||
unsigned callbacks = 0;
|
||||
size_t commands_after_replacement = 0;
|
||||
log.set_callback([&](const std::string &message) {
|
||||
if (callbacks == 0 && message.find("cleared Warning flag") != std::string::npos) {
|
||||
callbacks++;
|
||||
EXPECT_TRUE(first.results.empty()); // Status is reported before the completion.
|
||||
EXPECT_FALSE(hw.radio.cancel_transmit_async(&first)); // Already finished: no second completion.
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(replacement, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
commands_after_replacement = hw.transactions.size();
|
||||
}
|
||||
});
|
||||
{
|
||||
LoopBlockingGuard clock{nullptr, nullptr, millis()};
|
||||
hw.radio.loop();
|
||||
}
|
||||
ASSERT_EQ(callbacks, 1u);
|
||||
const auto expected = failed_command == 0 ? SX126xError::SX126X_ERROR_NONE : SX126xError::SX126X_ERROR_BUSY_TIMEOUT;
|
||||
EXPECT_EQ(first.results, (std::vector<SX126xError>{expected}));
|
||||
EXPECT_TRUE(second.results.empty());
|
||||
EXPECT_TRUE(hw.radio.is_transmitting());
|
||||
EXPECT_EQ(hw.transactions.size(), commands_after_replacement); // No stale IRQ clear or restoration.
|
||||
hw.irq = IRQ_TX_DONE;
|
||||
hw.radio.loop();
|
||||
ASSERT_EQ(replacement->results.size(), reuse_listener ? 2u : 1u);
|
||||
EXPECT_EQ(replacement->results.back(), SX126xError::SX126X_ERROR_NONE);
|
||||
EXPECT_FALSE(hw.radio.is_transmitting());
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(ReadSuccessReadFailureAndClearFailure, SX126xPollingReentry,
|
||||
::testing::Combine(::testing::Values(0u, 1u, 2u), ::testing::Bool()));
|
||||
|
||||
} // namespace esphome::sx126x::testing
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "sx126x_test_helpers.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <tuple>
|
||||
|
||||
namespace esphome::sx126x::testing {
|
||||
|
||||
struct RejectedCompletion final : SX126xTransmitListener {
|
||||
void on_transmit_complete(SX126xError result) override { ADD_FAILURE() << "Rejected transmission completed"; }
|
||||
};
|
||||
|
||||
class SX126xReceiveRetry : public ::testing::TestWithParam<std::tuple<bool, unsigned, bool>> {};
|
||||
|
||||
TEST_P(SX126xReceiveRetry, PersistentBusyIsRateLimitedAndRecoversWithoutNewEdge) {
|
||||
const auto [held_receive, failed_command, rollover] = GetParam();
|
||||
RadioHardware hw;
|
||||
PacketListener listener;
|
||||
RejectedCompletion tx;
|
||||
hw.configure(true, true);
|
||||
hw.radio.register_listener(&listener);
|
||||
hw.radio.call();
|
||||
hw.dio.internal = true;
|
||||
if (held_receive) {
|
||||
auto write = hw.cs.on_write;
|
||||
hw.cs.on_write = [&](bool high) {
|
||||
write(high);
|
||||
if (high && !hw.transactions.back().empty() && hw.transactions.back()[0] == RADIO_SET_STANDBY) {
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
}
|
||||
};
|
||||
EXPECT_EQ(hw.radio.start_transmit_async(&tx, hw.packet), SX126xError::SX126X_ERROR_BUSY);
|
||||
hw.cs.on_write = write;
|
||||
}
|
||||
hw.clear_trace();
|
||||
hw.irq = IRQ_RX_DONE;
|
||||
hw.fail_at = failed_command;
|
||||
const uint32_t start = rollover ? 0xFFFFFFF0u : 1000;
|
||||
{
|
||||
LoopBlockingGuard clock{nullptr, nullptr, start};
|
||||
hw.radio.call();
|
||||
}
|
||||
hw.expect_aborted(failed_command);
|
||||
EXPECT_EQ(hw.irq, IRQ_RX_DONE);
|
||||
const auto normal_busy = hw.busy.on_read;
|
||||
hw.busy.on_read = []() { return true; };
|
||||
for (uint32_t elapsed = 1; elapsed < 200; elapsed++) {
|
||||
LoopBlockingGuard clock{nullptr, nullptr, start + elapsed};
|
||||
hw.radio.call();
|
||||
}
|
||||
EXPECT_EQ(hw.transactions.size(), failed_command); // No SPI/BUSY waits inside the retry interval.
|
||||
{
|
||||
LoopBlockingGuard clock{nullptr, nullptr, start + 200};
|
||||
hw.radio.call();
|
||||
}
|
||||
ASSERT_EQ(hw.transactions.size(), failed_command + 1);
|
||||
EXPECT_TRUE(hw.transactions.back().empty());
|
||||
EXPECT_TRUE(hw.radio.is_in_loop_state());
|
||||
EXPECT_TRUE(hw.cs.level);
|
||||
EXPECT_EQ(hw.irq, IRQ_RX_DONE);
|
||||
EXPECT_TRUE(listener.packets.empty());
|
||||
hw.busy.on_read = normal_busy;
|
||||
hw.fail_at = 0;
|
||||
{
|
||||
LoopBlockingGuard clock{nullptr, nullptr, start + 399};
|
||||
hw.radio.call();
|
||||
EXPECT_EQ(hw.transactions.size(), failed_command + 1);
|
||||
}
|
||||
{
|
||||
LoopBlockingGuard clock{nullptr, nullptr, start + 400};
|
||||
hw.radio.call();
|
||||
}
|
||||
ASSERT_EQ(listener.packets.size(), 1u);
|
||||
EXPECT_EQ(listener.packets.front(), hw.packet);
|
||||
EXPECT_FALSE(hw.radio.status_has_warning());
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
EXPECT_EQ(hw.sends, 0u);
|
||||
hw.radio.call();
|
||||
EXPECT_EQ(listener.packets.size(), 1u);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(IdleAndHeldRxWithIrqAndPacketFailuresAcrossRollover, SX126xReceiveRetry,
|
||||
::testing::Combine(::testing::Bool(), ::testing::Values(1u, 2u), ::testing::Bool()));
|
||||
|
||||
TEST(SX126xReceiveRetry, SuccessfulConfigurationClearsBackoffAndOtherRadiosRemainIndependent) {
|
||||
RadioHardware failed, other;
|
||||
PacketListener recovered, unaffected;
|
||||
failed.configure(true, true);
|
||||
other.configure(true, true);
|
||||
failed.radio.register_listener(&recovered);
|
||||
other.radio.register_listener(&unaffected);
|
||||
failed.clear_trace();
|
||||
failed.fail_at = 1;
|
||||
failed.irq = IRQ_RX_DONE;
|
||||
LoopBlockingGuard clock{nullptr, nullptr, 1000};
|
||||
failed.radio.loop();
|
||||
other.irq = IRQ_RX_DONE;
|
||||
other.radio.loop();
|
||||
ASSERT_EQ(unaffected.packets.size(), 1u);
|
||||
EXPECT_TRUE(recovered.packets.empty());
|
||||
failed.clear_trace();
|
||||
failed.radio.configure();
|
||||
failed.irq = IRQ_RX_DONE;
|
||||
failed.radio.loop();
|
||||
ASSERT_EQ(recovered.packets.size(), 1u); // Explicit recovery does not wait for the old retry deadline.
|
||||
}
|
||||
|
||||
TEST(SX126xReceiveRetry, ReceiveBackoffDoesNotDelayTransmitCompletion) {
|
||||
struct Completion final : SX126xTransmitListener {
|
||||
void on_transmit_complete(SX126xError result) override { this->results.push_back(result); }
|
||||
std::vector<SX126xError> results;
|
||||
} tx;
|
||||
RadioHardware hw;
|
||||
hw.configure(true, true);
|
||||
hw.clear_trace();
|
||||
hw.fail_at = 1;
|
||||
hw.irq = IRQ_TX_DONE; // Failed read of an unrelated stale IRQ starts the RX retry gate.
|
||||
LoopBlockingGuard clock{nullptr, nullptr, millis()};
|
||||
hw.radio.loop();
|
||||
EXPECT_TRUE(hw.radio.status_has_warning());
|
||||
hw.clear_trace();
|
||||
hw.irq = 0;
|
||||
ASSERT_EQ(hw.radio.start_transmit_async(&tx, hw.packet), SX126xError::SX126X_ERROR_NONE);
|
||||
hw.radio.loop(); // Same cached tick, before the RX retry gate opens.
|
||||
EXPECT_EQ(tx.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
|
||||
EXPECT_TRUE(hw.receiving);
|
||||
}
|
||||
|
||||
} // namespace esphome::sx126x::testing
|
||||
@@ -0,0 +1,237 @@
|
||||
#pragma once
|
||||
#include <gtest/gtest.h>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include "esphome/components/sx126x/sx126x.h"
|
||||
namespace esphome::sx126x::testing {
|
||||
class TestPin : public GPIOPin {
|
||||
public:
|
||||
void setup() override {}
|
||||
void pin_mode(gpio::Flags flags) override {}
|
||||
gpio::Flags get_flags() const override { return gpio::FLAG_NONE; }
|
||||
size_t dump_summary(char *buffer, size_t len) const override { return snprintf(buffer, len, "test"); }
|
||||
bool digital_read() override {
|
||||
this->reads++;
|
||||
return this->on_read ? this->on_read() : this->level;
|
||||
}
|
||||
void digital_write(bool level) override {
|
||||
this->writes++;
|
||||
this->level = level;
|
||||
if (this->on_write) {
|
||||
this->on_write(level);
|
||||
}
|
||||
}
|
||||
bool is_internal() override { return this->internal; }
|
||||
|
||||
bool level{false};
|
||||
bool internal{false};
|
||||
unsigned reads{0};
|
||||
unsigned writes{0};
|
||||
std::function<bool()> on_read;
|
||||
std::function<void(bool)> on_write;
|
||||
};
|
||||
|
||||
// Exercise the real software SPI adapter and driver through GPIO pins. Each CS
|
||||
// assertion starts a command; BUSY can reject any one of those commands.
|
||||
class RadioHardware {
|
||||
public:
|
||||
RadioHardware() {
|
||||
this->cs.level = true;
|
||||
this->cs.on_write = [this](bool high) {
|
||||
if (!high) {
|
||||
EXPECT_FALSE(this->selected_);
|
||||
this->selected_ = true;
|
||||
this->transactions.emplace_back();
|
||||
this->bit_ = 0;
|
||||
this->byte_ = 0;
|
||||
this->busy.level = this->transactions.size() == this->fail_at;
|
||||
} else if (this->selected_) {
|
||||
this->selected_ = false;
|
||||
this->ends++;
|
||||
const auto &command = this->transactions.back();
|
||||
if (command.empty()) {
|
||||
return;
|
||||
}
|
||||
switch (command[0]) {
|
||||
case RADIO_SET_PACKETPARAMS:
|
||||
this->packet_params = command;
|
||||
break;
|
||||
case RADIO_CLR_IRQSTATUS:
|
||||
this->irq &= ~((uint16_t(command[1]) << 8) | command[2]);
|
||||
break;
|
||||
case RADIO_SET_TX:
|
||||
this->sends++;
|
||||
if (this->auto_complete) {
|
||||
this->irq |= IRQ_TX_DONE;
|
||||
}
|
||||
break;
|
||||
case RADIO_SET_RX:
|
||||
this->receiving = true;
|
||||
break;
|
||||
case RADIO_SET_SLEEP:
|
||||
case RADIO_SET_STANDBY:
|
||||
this->receiving = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (this->on_command) {
|
||||
this->on_command(command[0]);
|
||||
}
|
||||
}
|
||||
};
|
||||
this->busy.on_read = [this]() {
|
||||
EXPECT_TRUE(this->selected_); // NSS must wake the chip before waiting.
|
||||
return this->busy.level;
|
||||
};
|
||||
this->miso.on_read = [this]() {
|
||||
EXPECT_TRUE(this->selected_);
|
||||
EXPECT_FALSE(this->busy.level); // No clocks/data may follow a BUSY timeout.
|
||||
auto &command = this->transactions.back();
|
||||
const bool bit = (this->reply_byte_(command) >> (7 - this->bit_)) & 1;
|
||||
this->byte_ = (this->byte_ << 1) | this->mosi.level;
|
||||
if (++this->bit_ == 8) {
|
||||
command.push_back(this->byte_);
|
||||
this->byte_ = 0;
|
||||
this->bit_ = 0;
|
||||
}
|
||||
return bit;
|
||||
};
|
||||
this->dio.on_read = [this]() { return this->irq != 0; };
|
||||
this->rst.on_write = [this](bool high) {
|
||||
if (!high) {
|
||||
this->irq = 0;
|
||||
}
|
||||
};
|
||||
this->spi.set_clk(&this->clk);
|
||||
this->spi.set_mosi(&this->mosi);
|
||||
this->spi.set_miso(&this->miso);
|
||||
// call() runs setup() and marks the bus set up, so it accepts devices
|
||||
this->spi.call();
|
||||
this->radio.set_spi_parent(&this->spi);
|
||||
this->radio.set_cs_pin(&this->cs);
|
||||
this->radio.set_busy_pin(&this->busy);
|
||||
this->radio.set_dio1_pin(&this->dio);
|
||||
this->radio.set_rst_pin(&this->rst);
|
||||
this->radio.set_frequency(915000000);
|
||||
this->radio.set_tcxo_voltage(TCXO_CTRL_1_8V);
|
||||
this->radio.set_rf_switch(true);
|
||||
this->radio.set_crc_enable(true);
|
||||
this->radio.set_sync_value({0x14, 0x24});
|
||||
}
|
||||
~RadioHardware() {
|
||||
// The configuration retry interval (id 0) must not outlive the radio
|
||||
App.scheduler.cancel_interval(&this->radio, uint32_t{0});
|
||||
this->radio.spi_teardown();
|
||||
}
|
||||
|
||||
void configure(bool lora, bool rx, bool fixed = false) {
|
||||
this->radio.set_modulation(lora ? PACKET_TYPE_LORA : PACKET_TYPE_GFSK);
|
||||
this->radio.set_bandwidth(lora ? SX126X_BW_125000 : SX126X_BW_4800);
|
||||
this->radio.set_bitrate(4800);
|
||||
this->radio.set_rx_start(rx);
|
||||
this->radio.set_payload_length(fixed ? 3 : 0);
|
||||
this->radio.call(); // The normal component lifecycle runs setup().
|
||||
}
|
||||
|
||||
void clear_trace() {
|
||||
this->transactions.clear();
|
||||
this->ends = 0;
|
||||
this->fail_at = 0;
|
||||
this->busy.level = false;
|
||||
this->dio.reads = 0;
|
||||
}
|
||||
|
||||
void expect_aborted(size_t command) {
|
||||
ASSERT_EQ(this->transactions.size(), command);
|
||||
EXPECT_TRUE(this->transactions.back().empty());
|
||||
EXPECT_EQ(this->ends, command);
|
||||
EXPECT_TRUE(this->cs.level);
|
||||
}
|
||||
|
||||
SX126x radio;
|
||||
spi::SPIComponent spi;
|
||||
TestPin cs, busy, dio, rst, clk, mosi, miso;
|
||||
std::vector<std::vector<uint8_t>> transactions;
|
||||
std::vector<uint8_t> packet{0x11, 0x22, 0x33};
|
||||
std::vector<uint8_t> packet_params;
|
||||
size_t fail_at{0};
|
||||
size_t ends{0};
|
||||
unsigned sends{0};
|
||||
uint16_t irq{0};
|
||||
// Called as each command ends, with its opcode
|
||||
std::function<void(uint8_t)> on_command;
|
||||
bool receiving{false};
|
||||
bool auto_complete{true};
|
||||
bool supported_version{true};
|
||||
|
||||
protected:
|
||||
uint8_t reply_byte_(const std::vector<uint8_t> &command) {
|
||||
if (command.empty()) {
|
||||
return 0;
|
||||
}
|
||||
const size_t pos = command.size();
|
||||
switch (command[0]) {
|
||||
case RADIO_READ_REGISTER:
|
||||
if (pos >= 4) {
|
||||
static constexpr char VERSION[16] = "SX1262";
|
||||
return this->supported_version ? VERSION[pos - 4] : 0;
|
||||
}
|
||||
break;
|
||||
case RADIO_GET_IRQSTATUS:
|
||||
if (pos == 2) {
|
||||
return this->irq >> 8;
|
||||
}
|
||||
if (pos == 3) {
|
||||
return this->irq & 0xFF;
|
||||
}
|
||||
break;
|
||||
case RADIO_GET_PACKETSTATUS:
|
||||
if (pos == 2) {
|
||||
return 80;
|
||||
}
|
||||
if (pos == 3) {
|
||||
return 40;
|
||||
}
|
||||
if (pos == 4) {
|
||||
return 84;
|
||||
}
|
||||
break;
|
||||
case RADIO_GET_RXBUFFERSTATUS:
|
||||
if (pos == 2) {
|
||||
return this->packet.size();
|
||||
}
|
||||
if (pos == 3) {
|
||||
return 7;
|
||||
}
|
||||
break;
|
||||
case RADIO_READ_BUFFER:
|
||||
if (pos >= 3) {
|
||||
EXPECT_EQ(command[1], 7);
|
||||
return this->packet[pos - 3];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool selected_{false};
|
||||
uint8_t bit_{0};
|
||||
uint8_t byte_{0};
|
||||
};
|
||||
|
||||
class PacketListener : public SX126xListener {
|
||||
public:
|
||||
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override {
|
||||
this->packets.push_back(packet);
|
||||
this->rssi = rssi;
|
||||
this->snr = snr;
|
||||
}
|
||||
std::vector<std::vector<uint8_t>> packets;
|
||||
float rssi{};
|
||||
float snr{};
|
||||
};
|
||||
|
||||
} // namespace esphome::sx126x::testing
|
||||
@@ -10,7 +10,8 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.auto_load = ["climate_ir", "climate"]
|
||||
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
# Compiles in the Hisense ARG model, which climate.py adds only when it is configured
|
||||
# Compiles in the Hisense ARG and Hitachi models, which climate.py adds only when configured
|
||||
cg.add_define("USE_WHIRLPOOL_HISENSE_ARG")
|
||||
cg.add_define("USE_WHIRLPOOL_HITACHI_HCRA31NEWH")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -8,3 +8,8 @@ climate:
|
||||
model: HISENSE_ARG
|
||||
transmitter_id: xmitr
|
||||
receiver_id: rcvr
|
||||
- platform: whirlpool
|
||||
name: Whirlpool Hitachi HCRA31NEWH Climate
|
||||
model: HITACHI_HCRA31NEWH
|
||||
transmitter_id: xmitr
|
||||
receiver_id: rcvr
|
||||
@@ -252,6 +252,9 @@ TEST(WhirlpoolHisenseArg, SwingIsSentOnEveryTransmit) {
|
||||
|
||||
TEST(WhirlpoolDG11J13A, SwingIsSentOnlyWhenItChanges) {
|
||||
Fixture f(MODEL_DG11J1_3A);
|
||||
f.command(climate::CLIMATE_MODE_COOL, 24, climate::CLIMATE_FAN_LOW, climate::CLIMATE_SWING_OFF);
|
||||
EXPECT_EQ(f.last_frame()[2] & 0x80, 0) << "swing is already off";
|
||||
|
||||
f.command(climate::CLIMATE_MODE_COOL, 24, climate::CLIMATE_FAN_LOW, climate::CLIMATE_SWING_VERTICAL);
|
||||
Frame frame = f.last_frame();
|
||||
EXPECT_EQ(frame[2] & 0x80, 0x80);
|
||||
@@ -261,6 +264,9 @@ TEST(WhirlpoolDG11J13A, SwingIsSentOnlyWhenItChanges) {
|
||||
frame = f.last_frame();
|
||||
EXPECT_EQ(frame[2] & 0x80, 0);
|
||||
EXPECT_EQ(frame[8], 0x00);
|
||||
|
||||
f.command(climate::CLIMATE_MODE_COOL, 25, climate::CLIMATE_FAN_LOW, climate::CLIMATE_SWING_VERTICAL);
|
||||
EXPECT_EQ(f.last_frame()[2] & 0x80, 0) << "swing is already vertical";
|
||||
}
|
||||
|
||||
// Cool 24 C (step 8), fan low, powered on assumed
|
||||
@@ -385,4 +391,90 @@ TEST(WhirlpoolHisenseArg, EveryTransmitHasValidChecksums) {
|
||||
}
|
||||
}
|
||||
|
||||
// Cool 24 C (step 8), fan low, unit on
|
||||
static Frame hitachi_cool_frame() {
|
||||
return with_checksums({0xA9, 0x59, 0x03, 0x82, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x20,
|
||||
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x38, 0x08, 0x00});
|
||||
}
|
||||
|
||||
// The off frame: power bit, cool code, byte 15 0x01 and byte 18 0x28
|
||||
static Frame hitachi_off_frame() {
|
||||
return with_checksums({0xA9, 0x59, 0x07, 0x82, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x20,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x28, 0x08, 0x00});
|
||||
}
|
||||
|
||||
TEST(WhirlpoolHitachiHCRA31NEWH, CoolAndOffMatchKnownFrames) {
|
||||
Fixture f(MODEL_HITACHI_HCRA31NEWH);
|
||||
f.command(climate::CLIMATE_MODE_COOL, 24, climate::CLIMATE_FAN_LOW);
|
||||
// Turning on sends the full state with no power bit
|
||||
expect_frame(f.last_frame(), hitachi_cool_frame());
|
||||
|
||||
f.command(climate::CLIMATE_MODE_OFF, 24, climate::CLIMATE_FAN_LOW);
|
||||
expect_frame(f.last_frame(), hitachi_off_frame());
|
||||
|
||||
// A second off command still sends the power bit
|
||||
f.command(climate::CLIMATE_MODE_OFF, 24, climate::CLIMATE_FAN_LOW);
|
||||
EXPECT_EQ(f.last_frame()[2] & 0x04, 0x04);
|
||||
|
||||
f.command(climate::CLIMATE_MODE_HEAT, 24, climate::CLIMATE_FAN_LOW);
|
||||
const Frame frame = f.last_frame();
|
||||
EXPECT_EQ(frame[2] & 0x04, 0);
|
||||
EXPECT_EQ(frame[3], 0x80);
|
||||
EXPECT_EQ(frame[15], 0x06);
|
||||
EXPECT_EQ(frame[18], 0x38);
|
||||
}
|
||||
|
||||
TEST(WhirlpoolHitachiHCRA31NEWH, ReceivePowerIsAState) {
|
||||
Fixture f(MODEL_HITACHI_HCRA31NEWH);
|
||||
ASSERT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
|
||||
|
||||
// An on frame restores the mode even though it has no power bit
|
||||
EXPECT_TRUE(receive(f.climate, hitachi_cool_frame()));
|
||||
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_COOL);
|
||||
EXPECT_FLOAT_EQ(f.climate.target_temperature, 24);
|
||||
EXPECT_EQ(f.climate.fan_mode, climate::CLIMATE_FAN_LOW);
|
||||
// The same frame again keeps it on
|
||||
EXPECT_TRUE(receive(f.climate, hitachi_cool_frame()));
|
||||
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_COOL);
|
||||
|
||||
EXPECT_TRUE(receive(f.climate, hitachi_off_frame()));
|
||||
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
|
||||
// A repeated off frame does not toggle
|
||||
EXPECT_TRUE(receive(f.climate, hitachi_off_frame()));
|
||||
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
|
||||
}
|
||||
|
||||
TEST(WhirlpoolHitachiHCRA31NEWH, ReceiveChecksRemoteId) {
|
||||
Fixture hitachi(MODEL_HITACHI_HCRA31NEWH);
|
||||
EXPECT_FALSE(receive(hitachi.climate, hisense_cool_frame()));
|
||||
Fixture whirlpool(MODEL_DG11J1_91);
|
||||
EXPECT_FALSE(receive(whirlpool.climate, hitachi_cool_frame()));
|
||||
}
|
||||
|
||||
TEST(WhirlpoolHitachiHCRA31NEWH, ReceivedTemperatureIsClamped) {
|
||||
Fixture f(MODEL_HITACHI_HCRA31NEWH);
|
||||
Frame frame = hitachi_cool_frame();
|
||||
frame[3] = 0xF2; // step 15 would be 31 C
|
||||
EXPECT_TRUE(receive(f.climate, with_checksums(frame)));
|
||||
EXPECT_FLOAT_EQ(f.climate.target_temperature, 30);
|
||||
}
|
||||
|
||||
TEST(WhirlpoolDG11J13A, TargetIsClampedAndRoundedToWhatWasSent) {
|
||||
Fixture f(MODEL_DG11J1_3A);
|
||||
f.command(climate::CLIMATE_MODE_COOL, 35, climate::CLIMATE_FAN_LOW);
|
||||
EXPECT_FLOAT_EQ(f.climate.target_temperature, 32);
|
||||
f.command(climate::CLIMATE_MODE_COOL, 24.4f, climate::CLIMATE_FAN_LOW);
|
||||
EXPECT_FLOAT_EQ(f.climate.target_temperature, 24);
|
||||
}
|
||||
|
||||
TEST(WhirlpoolHitachiHCRA31NEWH, ReceiveRejectsShortFrame) {
|
||||
Fixture f(MODEL_HITACHI_HCRA31NEWH);
|
||||
remote_base::RawTimings raw = waveform(hitachi_cool_frame());
|
||||
// Cut after the second divider: 14 bytes, no footer
|
||||
raw.resize(2 + 14 * 16 + 2 * 2);
|
||||
remote_base::RemoteReceiveData data(raw, 25, remote_base::TOLERANCE_MODE_PERCENTAGE);
|
||||
EXPECT_FALSE(static_cast<remote_base::RemoteReceiverListener &>(f.climate).on_receive(data));
|
||||
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
|
||||
}
|
||||
|
||||
} // namespace esphome::whirlpool::testing
|
||||
@@ -0,0 +1,23 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_NAME
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/tests"]
|
||||
MULTI_CONF = True
|
||||
|
||||
reboot_hook_recorder_ns = cg.esphome_ns.namespace("reboot_hook_recorder")
|
||||
RebootHookRecorder = reboot_hook_recorder_ns.class_("RebootHookRecorder", cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(RebootHookRecorder),
|
||||
cv.Required(CONF_NAME): cv.string,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_name(config[CONF_NAME]))
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#include "reboot_hook_recorder.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::reboot_hook_recorder {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "reboot_hook_recorder");
|
||||
|
||||
void RebootHookRecorder::record_(const char *hook) { ESP_LOGI(TAG, "HOOK %s %s", this->name_, hook); }
|
||||
|
||||
} // namespace esphome::reboot_hook_recorder
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::reboot_hook_recorder {
|
||||
|
||||
/// Logs every shutdown lifecycle hook it receives, so a test can check their order.
|
||||
class RebootHookRecorder : public Component {
|
||||
public:
|
||||
void set_name(const char *name) { this->name_ = name; }
|
||||
|
||||
void on_safe_shutdown() override { this->record_("on_safe_shutdown"); }
|
||||
void on_shutdown() override { this->record_("on_shutdown"); }
|
||||
// Needs a second call to finish, like a component waiting for a connection to close
|
||||
bool teardown() override {
|
||||
if (++this->teardown_calls_ < 2) {
|
||||
this->record_("teardown_pending");
|
||||
return false;
|
||||
}
|
||||
if (this->teardown_calls_ == 2)
|
||||
this->record_("teardown_done");
|
||||
return true;
|
||||
}
|
||||
void on_powerdown() override { this->record_("on_powerdown"); }
|
||||
|
||||
protected:
|
||||
void record_(const char *hook);
|
||||
|
||||
const char *name_{""};
|
||||
uint8_t teardown_calls_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::reboot_hook_recorder
|
||||
@@ -0,0 +1,19 @@
|
||||
esphome:
|
||||
name: reboot-hooks-forced
|
||||
host:
|
||||
logger:
|
||||
level: DEBUG
|
||||
api:
|
||||
# Reboot once the test has connected, so that it sees every hook
|
||||
on_client_connected:
|
||||
- delay: 100ms
|
||||
- lambda: App.reboot();
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
reboot_hook_recorder:
|
||||
- name: first
|
||||
- name: second
|
||||
@@ -0,0 +1,19 @@
|
||||
esphome:
|
||||
name: reboot-hooks-safe
|
||||
host:
|
||||
logger:
|
||||
level: DEBUG
|
||||
api:
|
||||
# Reboot once the test has connected, so that it sees every hook
|
||||
on_client_connected:
|
||||
- delay: 100ms
|
||||
- lambda: App.safe_reboot();
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
reboot_hook_recorder:
|
||||
- name: first
|
||||
- name: second
|
||||
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user