320 lines
11 KiB
Python
320 lines
11 KiB
Python
import logging
|
|
from pathlib import Path
|
|
import re
|
|
from string import ascii_letters, digits
|
|
import subprocess
|
|
|
|
import esphome.codegen as cg
|
|
import esphome.config_validation as cv
|
|
from esphome.const import (
|
|
CONF_BOARD,
|
|
CONF_ENABLE_FULL_PRINTF,
|
|
CONF_FRAMEWORK,
|
|
CONF_PLATFORM_VERSION,
|
|
CONF_SOURCE,
|
|
CONF_VERSION,
|
|
CONF_WATCHDOG_TIMEOUT,
|
|
KEY_CORE,
|
|
KEY_FRAMEWORK_VERSION,
|
|
KEY_TARGET_FRAMEWORK,
|
|
KEY_TARGET_PLATFORM,
|
|
PLATFORM_RP2040,
|
|
ThreadModel,
|
|
)
|
|
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
|
|
from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed
|
|
|
|
from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns
|
|
|
|
# force import gpio to register pin schema
|
|
from .gpio import rp2040_pin_to_code # noqa
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
CODEOWNERS = ["@jesserockz"]
|
|
AUTO_LOAD = ["preferences"]
|
|
IS_TARGET_PLATFORM = True
|
|
|
|
|
|
def set_core_data(config):
|
|
CORE.data[KEY_RP2040] = {}
|
|
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_RP2040
|
|
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino"
|
|
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
|
|
config[CONF_FRAMEWORK][CONF_VERSION]
|
|
)
|
|
CORE.data[KEY_RP2040][KEY_BOARD] = config[CONF_BOARD]
|
|
|
|
CORE.data[KEY_RP2040][KEY_PIO_FILES] = {}
|
|
|
|
return config
|
|
|
|
|
|
def get_download_types(storage_json):
|
|
return [
|
|
{
|
|
"title": "UF2 factory format",
|
|
"description": "For copying to RP2040 over USB.",
|
|
"file": "firmware.uf2",
|
|
"download": f"{storage_json.name}.factory.uf2",
|
|
},
|
|
{
|
|
"title": "OTA format",
|
|
"description": "For OTA updating a device.",
|
|
"file": "firmware.ota.bin",
|
|
"download": f"{storage_json.name}.ota.bin",
|
|
},
|
|
]
|
|
|
|
|
|
def _format_framework_arduino_version(ver: cv.Version) -> str:
|
|
# The most recent releases have not been uploaded to platformio so grabbing them directly from
|
|
# the GitHub release is one path forward for now.
|
|
return f"https://github.com/earlephilhower/arduino-pico/releases/download/{ver}/rp2040-{ver}.zip"
|
|
|
|
# format the given arduino (https://github.com/earlephilhower/arduino-pico/releases) version to
|
|
# a PIO earlephilhower/framework-arduinopico value
|
|
# List of package versions: https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico
|
|
# return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
|
|
|
|
|
def _parse_platform_version(value):
|
|
value = cv.string(value)
|
|
if value.startswith("http"):
|
|
return value
|
|
|
|
return f"https://github.com/maxgerhardt/platform-raspberrypi.git#{value}"
|
|
|
|
|
|
# NOTE: Keep this in mind when updating the recommended version:
|
|
# * The new version needs to be thoroughly validated before changing the
|
|
# recommended version as otherwise a bunch of devices could be bricked
|
|
# * For all constants below, update platformio.ini (in this repo)
|
|
# and platformio.ini/platformio-lint.ini in the esphome-docker-base repository
|
|
|
|
# The default/recommended arduino framework version
|
|
# - https://github.com/earlephilhower/arduino-pico/releases
|
|
# - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico
|
|
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(5, 5, 1)
|
|
|
|
# The raspberrypi platform version to use for arduino frameworks
|
|
# - https://github.com/maxgerhardt/platform-raspberrypi/tags
|
|
RECOMMENDED_ARDUINO_PLATFORM_VERSION = "v1.4.0-gcc14-arduinopico460"
|
|
|
|
|
|
def _arduino_check_versions(value):
|
|
value = value.copy()
|
|
lookups = {
|
|
"dev": (cv.Version(5, 5, 1), "https://github.com/earlephilhower/arduino-pico"),
|
|
"latest": (cv.Version(5, 5, 1), None),
|
|
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
|
|
}
|
|
|
|
if value[CONF_VERSION] in lookups:
|
|
if CONF_SOURCE in value:
|
|
raise cv.Invalid(
|
|
"Framework version needs to be explicitly specified when custom source is used."
|
|
)
|
|
|
|
version, source = lookups[value[CONF_VERSION]]
|
|
else:
|
|
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
|
|
source = value.get(CONF_SOURCE, None)
|
|
|
|
value[CONF_VERSION] = str(version)
|
|
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
|
|
|
|
value[CONF_PLATFORM_VERSION] = value.get(
|
|
CONF_PLATFORM_VERSION,
|
|
_parse_platform_version(RECOMMENDED_ARDUINO_PLATFORM_VERSION),
|
|
)
|
|
|
|
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
|
|
_LOGGER.warning(
|
|
"The selected Arduino framework version is not the recommended one."
|
|
)
|
|
|
|
return value
|
|
|
|
|
|
ARDUINO_FRAMEWORK_SCHEMA = cv.All(
|
|
cv.Schema(
|
|
{
|
|
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
|
|
cv.Optional(CONF_SOURCE): cv.string_strict,
|
|
cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
|
|
}
|
|
),
|
|
_arduino_check_versions,
|
|
)
|
|
|
|
CONFIG_SCHEMA = cv.All(
|
|
cv.Schema(
|
|
{
|
|
cv.Required(CONF_BOARD): cv.string_strict,
|
|
cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA,
|
|
cv.Optional(CONF_WATCHDOG_TIMEOUT, default="8388ms"): cv.All(
|
|
cv.positive_time_period_milliseconds,
|
|
cv.Range(max=cv.TimePeriod(milliseconds=8388)),
|
|
),
|
|
cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean,
|
|
}
|
|
),
|
|
set_core_data,
|
|
)
|
|
|
|
|
|
@coroutine_with_priority(CoroPriority.PLATFORM)
|
|
async def to_code(config):
|
|
cg.add(rp2040_ns.setup_preferences())
|
|
|
|
# Allow LDF to properly discover dependency including those in preprocessor
|
|
# conditionals
|
|
cg.add_platformio_option("lib_ldf_mode", "chain+")
|
|
cg.add_platformio_option("lib_compat_mode", "strict")
|
|
cg.add_platformio_option("board", config[CONF_BOARD])
|
|
cg.add_build_flag("-DUSE_RP2040")
|
|
cg.add_define("USE_NATIVE_64BIT_TIME")
|
|
cg.set_cpp_standard("gnu++20")
|
|
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
|
cg.add_define("ESPHOME_VARIANT", "RP2040")
|
|
cg.add_define(ThreadModel.SINGLE)
|
|
|
|
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
|
|
|
|
conf = config[CONF_FRAMEWORK]
|
|
cg.add_platformio_option("framework", "arduino")
|
|
cg.add_build_flag("-DUSE_ARDUINO")
|
|
cg.add_build_flag("-DUSE_RP2040_FRAMEWORK_ARDUINO")
|
|
# cg.add_build_flag("-DPICO_BOARD=pico_w")
|
|
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
|
cg.add_platformio_option(
|
|
"platform_packages",
|
|
[
|
|
f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}",
|
|
],
|
|
)
|
|
|
|
# Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r
|
|
# (~9.2 KB). See printf_stubs.cpp for implementation.
|
|
if config.get(CONF_ENABLE_FULL_PRINTF):
|
|
cg.add_define("USE_FULL_PRINTF")
|
|
else:
|
|
for symbol in ("vprintf", "printf", "fprintf"):
|
|
cg.add_build_flag(f"-Wl,--wrap={symbol}")
|
|
|
|
cg.add_platformio_option("board_build.core", "earlephilhower")
|
|
# In testing mode, use all flash for sketch to allow linking grouped component tests.
|
|
# Real RP2040 hardware uses 1MB filesystem + 1MB sketch, but CI tests may combine
|
|
# many components that exceed the 1MB sketch partition.
|
|
cg.add_platformio_option(
|
|
"board_build.filesystem_size", "0m" if CORE.testing_mode else "1m"
|
|
)
|
|
|
|
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
|
cg.add_define(
|
|
"USE_ARDUINO_VERSION_CODE",
|
|
cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"),
|
|
)
|
|
|
|
cg.add_define("USE_RP2040_WATCHDOG_TIMEOUT", config[CONF_WATCHDOG_TIMEOUT])
|
|
cg.add_define("USE_RP2040_CRASH_HANDLER")
|
|
|
|
|
|
def add_pio_file(component: str, key: str, data: str):
|
|
try:
|
|
cv.validate_id_name(key)
|
|
except cv.Invalid as e:
|
|
raise EsphomeError(
|
|
f"[{component}] Invalid PIO key: {key}. Allowed characters: [{ascii_letters}{digits}_]\nPlease report an issue https://github.com/esphome/esphome/issues"
|
|
) from e
|
|
CORE.data[KEY_RP2040][KEY_PIO_FILES][key] = data
|
|
|
|
|
|
def generate_pio_files() -> bool:
|
|
import shutil
|
|
|
|
shutil.rmtree(CORE.relative_build_path("src/pio"), ignore_errors=True)
|
|
|
|
includes: list[str] = []
|
|
files = CORE.data[KEY_RP2040][KEY_PIO_FILES]
|
|
if not files:
|
|
return False
|
|
for key, data in files.items():
|
|
pio_path = CORE.build_path / "src" / "pio" / f"{key}.pio"
|
|
pio_path.parent.mkdir(parents=True, exist_ok=True)
|
|
write_file_if_changed(pio_path, data)
|
|
includes.append(f"pio/{key}.pio.h")
|
|
|
|
write_file_if_changed(
|
|
CORE.relative_build_path("src/pio_includes.h"),
|
|
"#pragma once\n" + "\n".join([f'#include "{include}"' for include in includes]),
|
|
)
|
|
|
|
dir = Path(__file__).parent
|
|
build_pio_file = dir / "build_pio.py.script"
|
|
copy_file_if_changed(
|
|
build_pio_file,
|
|
CORE.relative_build_path("build_pio.py"),
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
# Called by writer.py
|
|
def copy_files():
|
|
dir = Path(__file__).parent
|
|
post_build_file = dir / "post_build.py.script"
|
|
copy_file_if_changed(
|
|
post_build_file,
|
|
CORE.relative_build_path("post_build.py"),
|
|
)
|
|
if generate_pio_files():
|
|
path = CORE.relative_src_path("esphome.h")
|
|
content = read_file(path).rstrip("\n")
|
|
write_file_if_changed(path, content + '\n#include "pio_includes.h"\n')
|
|
|
|
|
|
# RP2040 crash handler stacktrace decoding
|
|
# Matches output from esphome/components/rp2040/crash_handler.cpp
|
|
_CRASH_RE = re.compile(r"CRASH DETECTED ON PREVIOUS BOOT")
|
|
_CRASH_ADDR_RE = re.compile(
|
|
r"(?:PC|LR|BT\d):\s+(0x[0-9a-fA-F]{8})\s+\((?:fault location|return address|stack backtrace)\)"
|
|
)
|
|
|
|
|
|
def _addr2line(tool: str, elf: Path, addr: str) -> str:
|
|
try:
|
|
result = subprocess.run(
|
|
[tool, "-pfiaC", "-e", str(elf), addr],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
return result.stdout.strip()
|
|
except (OSError, subprocess.CalledProcessError):
|
|
return f"{addr} (decode failed)"
|
|
|
|
|
|
def process_stacktrace(config, line: str, backtrace_state: bool) -> bool:
|
|
"""Decode RP2040 crash handler output using addr2line."""
|
|
if _CRASH_RE.search(line):
|
|
_LOGGER.error("RP2040 crash detected - decoding addresses")
|
|
return True
|
|
|
|
if backtrace_state:
|
|
if match := _CRASH_ADDR_RE.search(line):
|
|
from esphome.platformio_api import get_idedata
|
|
|
|
idedata = get_idedata(config)
|
|
if idedata.addr2line_path:
|
|
elf = idedata.firmware_elf_path
|
|
if elf.exists():
|
|
decoded = _addr2line(idedata.addr2line_path, elf, match.group(1))
|
|
_LOGGER.error(" %s => %s", match.group(1), decoded)
|
|
|
|
# Stop backtrace state after addr2line hint (last line of crash dump)
|
|
if "addr2line" in line:
|
|
return False
|
|
|
|
return backtrace_state
|