mirror of
https://github.com/esphome/esphome.git
synced 2026-09-30 00:10:22 +00:00
[ble_nus] Increase BLE MTU for NUS (#19629)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
pre-commit-ci-lite[bot]
J. Nick Koston
parent
1183beb1fb
commit
a000ce90bc
@@ -7,6 +7,7 @@ from esphome.components.uart import (
|
||||
uart_ns,
|
||||
)
|
||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
from esphome.components.zephyr_ble_server import request_ble_l2cap_mtu
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEBUG,
|
||||
@@ -61,6 +62,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
zephyr_add_prj_conf("BT_NUS", True)
|
||||
request_ble_l2cap_mtu(247) # DLE sweet spot: one NUS frame per LL PDU
|
||||
expose_log = config[CONF_TYPE] == CONF_LOGS
|
||||
cg.add(var.set_expose_log(expose_log))
|
||||
if expose_log:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.nrf52.framework import (
|
||||
@@ -7,9 +9,48 @@ from esphome.components.nrf52.framework import (
|
||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, Framework
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
# BLE LE Data Length Extension maximum LL PDU payload (BLE spec)
|
||||
_DLE_MAX_PDU = 251
|
||||
|
||||
DOMAIN = "zephyr_ble_server"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _BLEServerData:
|
||||
requested_l2cap_mtu: int = 0
|
||||
mtu_emitted: bool = False
|
||||
|
||||
|
||||
def _get_data() -> _BLEServerData:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = _BLEServerData()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _emit_ble_mtu() -> None:
|
||||
data = _get_data()
|
||||
data.mtu_emitted = True
|
||||
mtu = data.requested_l2cap_mtu
|
||||
if mtu > 0:
|
||||
zephyr_add_prj_conf("BT_L2CAP_TX_MTU", mtu)
|
||||
zephyr_add_prj_conf("BT_BUF_ACL_TX_SIZE", min(mtu + 4, _DLE_MAX_PDU))
|
||||
zephyr_add_prj_conf("BT_BUF_ACL_RX_SIZE", mtu + 4)
|
||||
|
||||
|
||||
def request_ble_l2cap_mtu(l2cap_mtu: int) -> None:
|
||||
"""Request a minimum BLE L2CAP MTU. The maximum of all callers wins."""
|
||||
data = _get_data()
|
||||
if data.mtu_emitted:
|
||||
raise RuntimeError("BLE L2CAP MTU requested after it was written to prj.conf")
|
||||
if data.requested_l2cap_mtu == 0:
|
||||
CORE.add_job(_emit_ble_mtu)
|
||||
data.requested_l2cap_mtu = max(data.requested_l2cap_mtu, l2cap_mtu)
|
||||
|
||||
|
||||
zephyr_ble_server_ns = cg.esphome_ns.namespace("zephyr_ble_server")
|
||||
BLEServer = zephyr_ble_server_ns.class_("BLEServer", cg.Component)
|
||||
|
||||
@@ -51,6 +92,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
zephyr_add_prj_conf("BT_SETTINGS", True)
|
||||
zephyr_add_prj_conf("BT_SMP_SC_ONLY", True)
|
||||
zephyr_add_prj_conf("BT_KEYS_OVERWRITE_OLDEST", True)
|
||||
request_ble_l2cap_mtu(65) # BT_SMP Kconfig default and range minimum
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.components.zephyr.const import (
|
||||
KEY_BOOTLOADER,
|
||||
KEY_SYSBUILD,
|
||||
)
|
||||
from esphome.components.zephyr_ble_server import request_ble_l2cap_mtu
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_HARDWARE_UART,
|
||||
@@ -144,6 +145,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
zephyr_add_prj_conf("MCUMGR_GRP_OS_MCUMGR_PARAMS", True)
|
||||
|
||||
zephyr_add_prj_conf("NCS_SAMPLE_MCUMGR_BT_OTA_DFU_SPEEDUP", True)
|
||||
request_ble_l2cap_mtu(498) # matches NCS_SAMPLE_MCUMGR_BT_OTA_DFU_SPEEDUP
|
||||
if CONF_HARDWARE_UART in transport:
|
||||
uart = UARTS[transport[CONF_HARDWARE_UART]]
|
||||
uart_name = uart[0]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
esphome:
|
||||
name: mtutest
|
||||
|
||||
nrf52:
|
||||
board: adafruit_itsybitsy_nrf52840
|
||||
bootloader: adafruit_nrf52_sd140_v6
|
||||
|
||||
zephyr_ble_server:
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: mtutest
|
||||
|
||||
nrf52:
|
||||
board: adafruit_itsybitsy_nrf52840
|
||||
bootloader: adafruit_nrf52_sd140_v6
|
||||
|
||||
logger:
|
||||
|
||||
ble_nus:
|
||||
type: logs
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: mtutest
|
||||
|
||||
nrf52:
|
||||
board: adafruit_itsybitsy_nrf52840
|
||||
bootloader: adafruit_nrf52_sd140_v6
|
||||
|
||||
logger:
|
||||
|
||||
zephyr_ble_server:
|
||||
|
||||
ble_nus:
|
||||
type: logs
|
||||
|
||||
ota:
|
||||
- platform: zephyr_mcumgr
|
||||
transport:
|
||||
ble: true
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Tests for the BLE L2CAP MTU that zephyr_ble_server writes to prj.conf."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.zephyr import zephyr_data
|
||||
from esphome.components.zephyr.const import KEY_PRJ_CONF
|
||||
from esphome.components.zephyr_ble_server import _emit_ble_mtu, request_ble_l2cap_mtu
|
||||
|
||||
MTU_KEYS = (
|
||||
"CONFIG_BT_L2CAP_TX_MTU",
|
||||
"CONFIG_BT_BUF_ACL_TX_SIZE",
|
||||
"CONFIG_BT_BUF_ACL_RX_SIZE",
|
||||
)
|
||||
|
||||
|
||||
def _mtu_values() -> tuple[int | None, ...]:
|
||||
prj_conf = zephyr_data()[KEY_PRJ_CONF][""]
|
||||
return tuple(prj_conf.get(key, (None,))[0] for key in MTU_KEYS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fixture", "expected"),
|
||||
[
|
||||
("no_request.yaml", (None, None, None)),
|
||||
("nus.yaml", (247, 251, 251)),
|
||||
# The largest request wins; only the TX buffer is capped at 251
|
||||
("nus_mcumgr.yaml", (498, 251, 502)),
|
||||
],
|
||||
)
|
||||
def test_l2cap_mtu_prj_conf(
|
||||
fixture: str,
|
||||
expected: tuple[int | None, ...],
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path(fixture))
|
||||
assert _mtu_values() == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_after_emit_raises(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path("no_request.yaml"))
|
||||
request_ble_l2cap_mtu(247)
|
||||
await _emit_ble_mtu()
|
||||
with pytest.raises(RuntimeError, match="after it was written"):
|
||||
request_ble_l2cap_mtu(498)
|
||||
Reference in New Issue
Block a user