mirror of
https://github.com/esphome/esphome.git
synced 2026-09-01 02:26:01 +00:00
[core] Warn on crystal frequency mismatch during serial upload
When flashing an ESP32 via serial, esptool prints the detected crystal frequency. This change parses that output in real-time and warns the user if it doesn't match the configured CONFIG_XTAL_FREQ in sdkconfig. This is particularly important for ESP32-C2 (ESP8684) boards where some modules use 26MHz crystals but the default sdkconfig assumes 40MHz, causing UART logging and other clock-dependent features to silently fail. Also adds a generic line_callbacks mechanism to RedirectText so future output-based checks can be added without modifying the class directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
df11e2765e
commit
c622ee6a6e
+53
-1
@@ -628,6 +628,50 @@ def _check_and_emit_build_info() -> None:
|
||||
)
|
||||
|
||||
|
||||
def _get_configured_xtal_freq() -> int | None:
|
||||
"""Read the configured crystal frequency from the sdkconfig file."""
|
||||
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
|
||||
if not sdkconfig_path.is_file():
|
||||
return None
|
||||
try:
|
||||
content = sdkconfig_path.read_text()
|
||||
for line in content.splitlines():
|
||||
if line.startswith("CONFIG_XTAL_FREQ="):
|
||||
return int(line.split("=", 1)[1])
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _make_crystal_freq_callback(
|
||||
configured_freq: int,
|
||||
) -> Callable[[str], str | None]:
|
||||
"""Create a callback that checks esptool crystal frequency output."""
|
||||
crystal_re = re.compile(r"Crystal frequency:\s+(\d+(?:\.\d+)?)\s*MHz")
|
||||
|
||||
def check_crystal_line(line: str) -> str | None:
|
||||
if match := crystal_re.search(line):
|
||||
detected = int(float(match.group(1)))
|
||||
if detected != configured_freq:
|
||||
return (
|
||||
f"\n\033[33mWARNING: Crystal frequency mismatch! "
|
||||
f"Device reports {detected}MHz but firmware is configured "
|
||||
f"for {configured_freq}MHz.\n"
|
||||
f"UART logging and other clock-dependent features will not "
|
||||
f"work correctly.\n"
|
||||
f"Set the correct crystal frequency with sdkconfig_options:\n"
|
||||
f" esp32:\n"
|
||||
f" framework:\n"
|
||||
f" sdkconfig_options:\n"
|
||||
f" CONFIG_XTAL_FREQ_{detected}: 'y'\n"
|
||||
f" CONFIG_XTAL_FREQ_{configured_freq}: 'n'\n"
|
||||
f' CONFIG_XTAL_FREQ: "{detected}"\033[0m\n\n'
|
||||
)
|
||||
return None
|
||||
|
||||
return check_crystal_line
|
||||
|
||||
|
||||
def upload_using_esptool(
|
||||
config: ConfigType, port: str, file: str, speed: int
|
||||
) -> str | int:
|
||||
@@ -656,6 +700,12 @@ def upload_using_esptool(
|
||||
|
||||
mcu = get_esp32_variant().lower()
|
||||
|
||||
line_callbacks = []
|
||||
if CORE.is_esp32:
|
||||
configured_freq = _get_configured_xtal_freq()
|
||||
if configured_freq is not None:
|
||||
line_callbacks.append(_make_crystal_freq_callback(configured_freq))
|
||||
|
||||
def run_esptool(baud_rate):
|
||||
cmd = [
|
||||
"esptool",
|
||||
@@ -680,7 +730,9 @@ def upload_using_esptool(
|
||||
if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
|
||||
import esptool
|
||||
|
||||
return run_external_command(esptool.main, *cmd) # pylint: disable=no-member
|
||||
return run_external_command(
|
||||
esptool.main, *cmd, line_callbacks=line_callbacks
|
||||
) # pylint: disable=no-member
|
||||
|
||||
return run_external_process(*cmd)
|
||||
|
||||
|
||||
+21
-4
@@ -124,7 +124,12 @@ ANSI_ESCAPE = re.compile(r"\033[@-_][0-?]*[ -/]*[@-~]")
|
||||
|
||||
|
||||
class RedirectText:
|
||||
def __init__(self, out, filter_lines=None):
|
||||
def __init__(
|
||||
self,
|
||||
out,
|
||||
filter_lines: str | None = None,
|
||||
line_callbacks: list[Callable[[str], str | None]] | None = None,
|
||||
) -> None:
|
||||
self._out = out
|
||||
if filter_lines is None:
|
||||
self._filter_pattern = None
|
||||
@@ -132,6 +137,7 @@ class RedirectText:
|
||||
pattern = r"|".join(r"(?:" + pattern + r")" for pattern in filter_lines)
|
||||
self._filter_pattern = re.compile(pattern)
|
||||
self._line_buffer = ""
|
||||
self._line_callbacks = line_callbacks or []
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self._out, item)
|
||||
@@ -180,6 +186,9 @@ class RedirectText:
|
||||
and (help_msg := get_esp32_arduino_flash_error_help())
|
||||
):
|
||||
self._write_color_replace(help_msg)
|
||||
for callback in self._line_callbacks:
|
||||
if msg := callback(line_without_end):
|
||||
self._write_color_replace(msg)
|
||||
else:
|
||||
self._write_color_replace(s)
|
||||
|
||||
@@ -193,7 +202,11 @@ class RedirectText:
|
||||
|
||||
|
||||
def run_external_command(
|
||||
func, *cmd, capture_stdout: bool = False, filter_lines: str = None
|
||||
func,
|
||||
*cmd,
|
||||
capture_stdout: bool = False,
|
||||
filter_lines: str = None,
|
||||
line_callbacks: list | None = None,
|
||||
) -> int | str:
|
||||
"""
|
||||
Run a function from an external package that acts like a main method.
|
||||
@@ -217,9 +230,13 @@ def run_external_command(
|
||||
_LOGGER.debug("Running: %s", full_cmd)
|
||||
|
||||
orig_stdout = sys.stdout
|
||||
sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines)
|
||||
sys.stdout = RedirectText(
|
||||
sys.stdout, filter_lines=filter_lines, line_callbacks=line_callbacks
|
||||
)
|
||||
orig_stderr = sys.stderr
|
||||
sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines)
|
||||
sys.stderr = RedirectText(
|
||||
sys.stderr, filter_lines=filter_lines, line_callbacks=line_callbacks
|
||||
)
|
||||
|
||||
if capture_stdout:
|
||||
cap_stdout = sys.stdout = io.StringIO()
|
||||
|
||||
Reference in New Issue
Block a user