[esp32] refactor esp32-vfs default configs to FINAL co-routine & add some defaults (#17337)

Co-authored-by: Oliver Kleinecke <kleinecke.oliver@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: storage split <ai@local>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
Oliver Kleinecke
2026-08-07 12:36:59 -05:00
committed by GitHub
co-authored by Oliver Kleinecke Claude Sonnet 4.6 Copilot Autofix powered by AI storage split pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
parent c0e70d9beb
commit b326cefea7
2 changed files with 225 additions and 52 deletions
+66 -52
View File
@@ -2248,6 +2248,62 @@ async def _add_yaml_idf_components(components: list[ConfigType]):
)
@coroutine_with_priority(CoroPriority.FINAL)
async def _reconcile_vfs_fatfs_sdkconfig(
disable_vfs_termios: bool,
disable_vfs_select: bool,
disable_vfs_dir: bool,
disable_fatfs: bool,
) -> None:
"""Reconcile VFS/FATFS sdkconfig flags after all require_*() calls; user sdkconfig_options win."""
opts = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
def set_opt(name: str, value: SdkconfigValueType) -> None:
# User sdkconfig_options (applied during to_code) win.
if name not in opts:
add_idf_sdkconfig_option(name, value)
# USB Serial JTAG VFS needs termios (require_vfs_termios(), e.g. logger). ~1.8KB flash when off.
if CORE.data.get(KEY_VFS_TERMIOS_REQUIRED, False):
set_opt("CONFIG_VFS_SUPPORT_TERMIOS", True)
else:
set_opt("CONFIG_VFS_SUPPORT_TERMIOS", not disable_vfs_termios)
# VFS select is only needed for UART/eventfd fds (require_vfs_select(), e.g. openthread);
# sockets use lwip_select() either way. ~2.7KB flash when off.
if CORE.data.get(KEY_VFS_SELECT_REQUIRED, False):
set_opt("CONFIG_VFS_SUPPORT_SELECT", True)
else:
set_opt("CONFIG_VFS_SUPPORT_SELECT", not disable_vfs_select)
# Directory functions: opendir/readdir/mkdir etc. (require_vfs_dir()). ~0.5KB flash when off.
if CORE.data.get(KEY_VFS_DIR_REQUIRED, False):
set_opt("CONFIG_VFS_SUPPORT_DIR", True)
else:
set_opt("CONFIG_VFS_SUPPORT_DIR", not disable_vfs_dir)
# FATFS (require_fatfs()): LFN + one volume per esp_vfs_fat mount. Defaults only;
# sdkconfig_options override. FATFS_LONG_FILENAMES is a Kconfig choice -- if the user set
# any member, leave the group alone. LFN_HEAP allocates per LFN op; LFN_STACK uses stack.
lfn_keys = (
"CONFIG_FATFS_LFN_NONE",
"CONFIG_FATFS_LFN_HEAP",
"CONFIG_FATFS_LFN_STACK",
)
user_picked_lfn = any(k in opts for k in lfn_keys)
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
if not user_picked_lfn:
set_opt("CONFIG_FATFS_LFN_NONE", False)
set_opt("CONFIG_FATFS_LFN_HEAP", True)
set_opt("CONFIG_FATFS_MAX_LFN", 255)
set_opt("CONFIG_FATFS_VOLUME_COUNT", 4)
elif disable_fatfs:
if not user_picked_lfn:
set_opt("CONFIG_FATFS_LFN_NONE", True)
# Kconfig range is [1,10]; 0 gets clamped to the default.
set_opt("CONFIG_FATFS_VOLUME_COUNT", 1)
@coroutine_with_priority(CoroPriority.FINAL - 1)
async def _finalize_arduino_aware_flags():
"""Build flags that depend on whether arduino-esp32 is linked in.
@@ -2603,47 +2659,6 @@ async def to_code(config):
if advanced[CONF_DISABLE_LIBC_LOCKS_IN_IRAM]:
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
# Disable VFS support for termios (terminal I/O functions)
# USB Serial JTAG VFS functions require termios support.
# Components that need it (e.g., logger when USB_SERIAL_JTAG is supported but not selected
# as the logger output) call require_vfs_termios().
# Saves approximately 1.8KB of flash when disabled (default).
if CORE.data.get(KEY_VFS_TERMIOS_REQUIRED, False):
# Component requires VFS termios - force enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_TERMIOS", True)
else:
# No component needs it - allow user to control (default: disabled)
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
)
# Disable VFS support for select() with file descriptors
# ESPHome only uses select() with sockets via lwip_select(), which still works.
# VFS select is only needed for UART/eventfd file descriptors.
# Components that need it (e.g., openthread) call require_vfs_select().
# Saves approximately 2.7KB of flash when disabled (default).
if CORE.data.get(KEY_VFS_SELECT_REQUIRED, False):
# Component requires VFS select - force enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_SELECT", True)
else:
# No component needs it - allow user to control (default: disabled)
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_SELECT", not advanced[CONF_DISABLE_VFS_SUPPORT_SELECT]
)
# Disable VFS support for directory functions (opendir, readdir, mkdir, etc.)
# ESPHome doesn't use directory functions on ESP32.
# Components that need it (e.g., storage components) call require_vfs_dir().
# Saves approximately 0.5KB+ of flash when disabled (default).
if CORE.data.get(KEY_VFS_DIR_REQUIRED, False):
# Component requires VFS directory support - force enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_DIR", True)
else:
# No component needs it - allow user to control (default: disabled)
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR]
)
if use_platformio:
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
@@ -2878,6 +2893,16 @@ async def to_code(config):
# FINAL priority: runs after every network/coexistence request_*() call
CORE.add_job(_reconcile_network_sdkconfig)
# FINAL: require_*() calls can come from to_code at or below this priority, so an
# inline read would be iteration-order-dependent; reconcile once after every job ran.
CORE.add_job(
_reconcile_vfs_fatfs_sdkconfig,
advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS],
advanced[CONF_DISABLE_VFS_SUPPORT_SELECT],
advanced[CONF_DISABLE_VFS_SUPPORT_DIR],
advanced[CONF_DISABLE_FATFS],
)
# Disable regi2c control functions in IRAM
# Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
@@ -2893,17 +2918,6 @@ async def to_code(config):
):
add_idf_sdkconfig_option("CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM", True)
# Disable FATFS support
# Components that need FATFS (SD card, etc.) can call require_fatfs()
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
# Component called require_fatfs() - enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", False)
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 2)
elif advanced[CONF_DISABLE_FATFS]:
add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", True)
# Kconfig range is [1,10]; 0 gets clamped to the default.
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 1)
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
+159
View File
@@ -10,11 +10,16 @@ from typing import Any
import pytest
from esphome.components.esp32 import (
KEY_FATFS_REQUIRED,
KEY_VFS_DIR_REQUIRED,
KEY_VFS_SELECT_REQUIRED,
KEY_VFS_TERMIOS_REQUIRED,
VARIANT_ESP32,
VARIANTS,
NetworkSdkconfigData,
_ota_downgrade_protection_errors,
_reconcile_network_sdkconfig,
_reconcile_vfs_fatfs_sdkconfig,
)
from esphome.components.esp32.const import (
KEY_ESP32,
@@ -614,6 +619,160 @@ def test_reconcile_network_sdkconfig(
assert CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] == expected
@pytest.mark.parametrize(
("requires", "fatfs_required", "disables", "preset", "expected"),
[
# Nothing required and every disable_* flag off (NOT the shipped defaults, which
# disable everything): VFS enabled, FATFS left untouched entirely.
pytest.param(
{},
False,
(False, False, False, False),
{},
{
"CONFIG_VFS_SUPPORT_TERMIOS": True,
"CONFIG_VFS_SUPPORT_SELECT": True,
"CONFIG_VFS_SUPPORT_DIR": True,
},
id="nothing_disabled_nothing_required",
),
# The shipped out-of-the-box path: every disable_* flag defaults to True and nothing
# is required -- VFS off, FATFS at the smallest footprint (8.3 names, one volume).
pytest.param(
{},
False,
(True, True, True, True),
{},
{
"CONFIG_VFS_SUPPORT_TERMIOS": False,
"CONFIG_VFS_SUPPORT_SELECT": False,
"CONFIG_VFS_SUPPORT_DIR": False,
"CONFIG_FATFS_LFN_NONE": True,
"CONFIG_FATFS_VOLUME_COUNT": 1,
},
id="all_disabled_fatfs_fallback",
),
# A component's require_* beats the user's disable_* flag for every VFS feature.
pytest.param(
{
KEY_VFS_TERMIOS_REQUIRED: True,
KEY_VFS_SELECT_REQUIRED: True,
KEY_VFS_DIR_REQUIRED: True,
},
False,
(True, True, True, False),
{},
{
"CONFIG_VFS_SUPPORT_TERMIOS": True,
"CONFIG_VFS_SUPPORT_SELECT": True,
"CONFIG_VFS_SUPPORT_DIR": True,
},
id="require_beats_disable",
),
# A user sdkconfig_options preset wins over a require (the set_opt guard).
pytest.param(
{KEY_VFS_SELECT_REQUIRED: True},
False,
(False, False, False, False),
{"CONFIG_VFS_SUPPORT_SELECT": False},
{
"CONFIG_VFS_SUPPORT_TERMIOS": True,
"CONFIG_VFS_SUPPORT_SELECT": False,
"CONFIG_VFS_SUPPORT_DIR": True,
},
id="user_preset_wins_over_require",
),
# require_fatfs() with no user preset: long filenames on the heap, 255 chars,
# four volumes.
pytest.param(
{},
True,
(False, False, False, False),
{},
{
"CONFIG_VFS_SUPPORT_TERMIOS": True,
"CONFIG_VFS_SUPPORT_SELECT": True,
"CONFIG_VFS_SUPPORT_DIR": True,
"CONFIG_FATFS_LFN_NONE": False,
"CONFIG_FATFS_LFN_HEAP": True,
"CONFIG_FATFS_MAX_LFN": 255,
"CONFIG_FATFS_VOLUME_COUNT": 4,
},
id="fatfs_required_defaults",
),
# CONFIG_FATFS_LONG_FILENAMES is a Kconfig choice: a user picking any member
# (here LFN_STACK) leaves the whole group untouched -- no second =y in the choice.
pytest.param(
{},
True,
(False, False, False, False),
{"CONFIG_FATFS_LFN_STACK": "y"},
{
"CONFIG_VFS_SUPPORT_TERMIOS": True,
"CONFIG_VFS_SUPPORT_SELECT": True,
"CONFIG_VFS_SUPPORT_DIR": True,
"CONFIG_FATFS_LFN_STACK": "y",
"CONFIG_FATFS_VOLUME_COUNT": 4,
},
id="fatfs_user_lfn_stack_untouched",
),
# disable_fatfs (the shipped default) with a user LFN pick: the choice group is the
# user's -- no LFN_NONE=y written next to their member, only the volume fallback.
pytest.param(
{},
False,
(False, False, False, True),
{"CONFIG_FATFS_LFN_HEAP": "y"},
{
"CONFIG_VFS_SUPPORT_TERMIOS": True,
"CONFIG_VFS_SUPPORT_SELECT": True,
"CONFIG_VFS_SUPPORT_DIR": True,
"CONFIG_FATFS_LFN_HEAP": "y",
"CONFIG_FATFS_VOLUME_COUNT": 1,
},
id="disable_fatfs_user_lfn_untouched",
),
# Same for an explicit LFN_NONE preset: the group is the user's, only the volume
# count default is added.
pytest.param(
{},
True,
(False, False, False, False),
{"CONFIG_FATFS_LFN_NONE": "y"},
{
"CONFIG_VFS_SUPPORT_TERMIOS": True,
"CONFIG_VFS_SUPPORT_SELECT": True,
"CONFIG_VFS_SUPPORT_DIR": True,
"CONFIG_FATFS_LFN_NONE": "y",
"CONFIG_FATFS_VOLUME_COUNT": 4,
},
id="fatfs_user_lfn_none_untouched",
),
],
)
def test_reconcile_vfs_fatfs_sdkconfig(
set_core_config: SetCoreConfigCallable,
requires: dict[str, bool],
fatfs_required: bool,
disables: tuple[bool, bool, bool, bool],
preset: dict[str, Any],
expected: dict[str, Any],
) -> None:
"""The FINAL-priority reconciler resolves the VFS feature flags and the FATFS
defaults from the recorded require_* calls, with user sdkconfig_options winning
and the LFN Kconfig choice treated as one group."""
set_core_config(PlatformFramework.ESP32_IDF)
CORE.data[KEY_ESP32] = {KEY_SDKCONFIG_OPTIONS: dict(preset)}
if fatfs_required:
CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True
for key, value in requires.items():
CORE.data[key] = value
asyncio.run(_reconcile_vfs_fatfs_sdkconfig(*disables))
assert CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] == expected
def test_network_wifi_only_reconciles_end_to_end(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],