mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
Reserve saved values per component and check the shared total after codegen
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
|
||||
# Re-exported from the shared definition; here it indexes the BOARDS
|
||||
# metadata dicts, whose entries in boards.py spell the literal.
|
||||
from esphome.const import KEY_FLASH_SIZE # noqa: F401 # pylint: disable=unused-import
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
KEY_ESP8266 = "esp8266"
|
||||
KEY_BOARD = "board"
|
||||
@@ -32,25 +36,68 @@ PREFERENCES_FLASH_WORDS = 128
|
||||
PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE = 64
|
||||
|
||||
|
||||
KEY_PREFERENCES = "preferences"
|
||||
# Words of RTC memory open to saved values; the crash handler keeps the rest
|
||||
PREFERENCES_RTC_WORDS = PREFERENCES_RTC_NORMAL_WORDS + PREFERENCES_RTC_EBOOT_WORDS
|
||||
|
||||
|
||||
def preference_words(value_bytes: int) -> int:
|
||||
"""Words a saved value takes, checksum word included."""
|
||||
return (value_bytes + 3) // 4 + 1
|
||||
|
||||
|
||||
def preference_slots(value_bytes: int, in_flash: bool, restore_from_flash: bool) -> int:
|
||||
"""How many values of this size fit where make_preference() puts them."""
|
||||
words = preference_words(value_bytes)
|
||||
if in_flash or restore_from_flash:
|
||||
store = (
|
||||
PREFERENCES_FLASH_WORDS
|
||||
if restore_from_flash
|
||||
else PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE
|
||||
)
|
||||
return store // words
|
||||
return sum(
|
||||
region // words
|
||||
for region in (PREFERENCES_RTC_NORMAL_WORDS, PREFERENCES_RTC_EBOOT_WORDS)
|
||||
def reserve_preferences(count: int, value_bytes: int, in_flash: bool = False) -> None:
|
||||
"""Record saved values a component creates, so all of them are checked to fit; call from to_code."""
|
||||
if not CORE.is_esp8266:
|
||||
return
|
||||
reservations = CORE.data.setdefault(KEY_ESP8266, {}).setdefault(KEY_PREFERENCES, [])
|
||||
if not reservations:
|
||||
CORE.add_job(check_preferences_fit)
|
||||
reservations.append((count, preference_words(value_bytes), in_flash))
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def check_preferences_fit() -> None:
|
||||
"""Fail the build when the reserved saved values cannot fit the space preferences.cpp has."""
|
||||
restore_from_flash = CORE.config[KEY_ESP8266].get(CONF_RESTORE_FROM_FLASH, False)
|
||||
flash_store = (
|
||||
PREFERENCES_FLASH_WORDS
|
||||
if restore_from_flash
|
||||
else PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE
|
||||
)
|
||||
rtc_words = flash_words = 0
|
||||
for count, words, in_flash in CORE.data[KEY_ESP8266][KEY_PREFERENCES]:
|
||||
if in_flash or restore_from_flash:
|
||||
if words > flash_store:
|
||||
raise EsphomeError(
|
||||
f"A saved value of {words} words does not fit the ESP8266 flash store of {flash_store} words"
|
||||
)
|
||||
flash_words += count * words
|
||||
else:
|
||||
# A value cannot span the two RTC regions, so the larger one bounds it
|
||||
if words > PREFERENCES_RTC_NORMAL_WORDS:
|
||||
raise EsphomeError(
|
||||
f"A saved value of {words} words does not fit an ESP8266 RTC region of "
|
||||
f"{PREFERENCES_RTC_NORMAL_WORDS} words"
|
||||
)
|
||||
rtc_words += count * words
|
||||
if flash_words > flash_store:
|
||||
raise EsphomeError(
|
||||
f"Saved values need {flash_words} words but the ESP8266 flash store has {flash_store}; "
|
||||
"use fewer or smaller saved values"
|
||||
)
|
||||
if rtc_words > PREFERENCES_RTC_WORDS:
|
||||
raise EsphomeError(
|
||||
f"Saved values need {rtc_words} words but ESP8266 RTC memory has {PREFERENCES_RTC_WORDS}; "
|
||||
"use fewer or smaller saved values, or set restore_from_flash: true"
|
||||
)
|
||||
if rtc_words > PREFERENCES_RTC_NORMAL_WORDS:
|
||||
_LOGGER.warning(
|
||||
"Saved values need %d of the %d words of ESP8266 RTC memory; values that straddle the two regions "
|
||||
"and saved settings that are not counted here may not all fit",
|
||||
rtc_words,
|
||||
PREFERENCES_RTC_WORDS,
|
||||
)
|
||||
|
||||
|
||||
# esp8266 namespace is already defined by arduino, manually prefix esphome
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
"""The config-time fit rule must match make_preference() in preferences.cpp."""
|
||||
"""The config-time fit check must agree with make_preference() in preferences.cpp."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp8266.const import preference_slots, preference_words
|
||||
from esphome.components.esp8266.const import (
|
||||
KEY_ESP8266,
|
||||
KEY_PREFERENCES,
|
||||
check_preferences_fit,
|
||||
preference_words,
|
||||
reserve_preferences,
|
||||
)
|
||||
from esphome.const import KEY_CORE, KEY_TARGET_PLATFORM
|
||||
from esphome.core import CORE, EsphomeError
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def esp8266_core() -> None:
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp8266"}
|
||||
CORE.data[KEY_ESP8266] = {}
|
||||
CORE.config = {KEY_ESP8266: {"restore_from_flash": False}}
|
||||
|
||||
|
||||
def _check() -> None:
|
||||
asyncio.run(check_preferences_fit())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -14,18 +35,39 @@ def test_preference_words_rounds_up_and_adds_the_checksum(
|
||||
assert preference_words(value_bytes) == words
|
||||
|
||||
|
||||
def test_rtc_values_cannot_straddle_the_regions() -> None:
|
||||
# 25 words each: 3 fit in the 78-word region and 1 in the 32-word one
|
||||
assert preference_slots(94, in_flash=False, restore_from_flash=False) == 4
|
||||
# 32 words each: 2 + 1, where 110 // 32 would also say 3
|
||||
assert preference_slots(124, in_flash=False, restore_from_flash=False) == 3
|
||||
# 39 words each: 2 + 0, where 110 // 39 would say 2 as well but the eboot region is skipped
|
||||
assert preference_slots(152, in_flash=False, restore_from_flash=False) == 2
|
||||
# Larger than the biggest region
|
||||
assert preference_slots(400, in_flash=False, restore_from_flash=False) == 0
|
||||
def test_reservations_from_several_components_add_up() -> None:
|
||||
reserve_preferences(2, 94) # 50 words
|
||||
reserve_preferences(2, 94) # another component, 100 in total
|
||||
_check()
|
||||
reserve_preferences(1, 94) # 125 > 110
|
||||
with pytest.raises(EsphomeError, match="RTC memory has 110"):
|
||||
_check()
|
||||
|
||||
|
||||
def test_flash_store_size_follows_restore_from_flash() -> None:
|
||||
assert preference_slots(94, in_flash=True, restore_from_flash=True) == 5
|
||||
assert preference_slots(94, in_flash=False, restore_from_flash=True) == 5
|
||||
assert preference_slots(94, in_flash=True, restore_from_flash=False) == 2
|
||||
def test_a_value_cannot_exceed_the_larger_rtc_region() -> None:
|
||||
reserve_preferences(1, 400)
|
||||
with pytest.raises(EsphomeError, match="RTC region"):
|
||||
_check()
|
||||
|
||||
|
||||
def test_flash_store_follows_restore_from_flash() -> None:
|
||||
CORE.config[KEY_ESP8266]["restore_from_flash"] = True
|
||||
reserve_preferences(5, 94) # 125 of 128 words
|
||||
_check()
|
||||
reserve_preferences(1, 94)
|
||||
with pytest.raises(EsphomeError, match="flash store has 128"):
|
||||
_check()
|
||||
|
||||
|
||||
def test_explicit_flash_values_use_the_small_store_without_restore() -> None:
|
||||
reserve_preferences(2, 94, in_flash=True) # 50 of 64 words
|
||||
_check()
|
||||
reserve_preferences(1, 94, in_flash=True)
|
||||
with pytest.raises(EsphomeError, match="flash store has 64"):
|
||||
_check()
|
||||
|
||||
|
||||
def test_reserving_schedules_the_check_once() -> None:
|
||||
reserve_preferences(1, 4)
|
||||
reserve_preferences(1, 4)
|
||||
assert len(CORE.data[KEY_ESP8266][KEY_PREFERENCES]) == 2
|
||||
|
||||
Reference in New Issue
Block a user