feat(factory): add local flashing toolkit and secure OTA defaults
This commit is contained in:
297
tools/esptool-factory/factory_gui/config.py
Normal file
297
tools/esptool-factory/factory_gui/config.py
Normal file
@@ -0,0 +1,297 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from factory_private_config import get_optional, get_required, load_private_config
|
||||
|
||||
from .models import DeviceCheckConfig, FlashPrivateConfig
|
||||
|
||||
_ALLOWED_FLASH_MODE = {"qio", "qout", "dio", "dout", "keep"}
|
||||
_ALLOWED_FLASH_FREQ = {
|
||||
"keep",
|
||||
"80m",
|
||||
"60m",
|
||||
"48m",
|
||||
"40m",
|
||||
"30m",
|
||||
"26m",
|
||||
"24m",
|
||||
"20m",
|
||||
"16m",
|
||||
"15m",
|
||||
"12m",
|
||||
}
|
||||
_ALLOWED_FLASH_SIZE = {
|
||||
"keep",
|
||||
"detect",
|
||||
"256kb",
|
||||
"512kb",
|
||||
"1mb",
|
||||
"2mb",
|
||||
"2mb-c1",
|
||||
"4mb",
|
||||
"4mb-c1",
|
||||
"8mb",
|
||||
"16mb",
|
||||
"32mb",
|
||||
"64mb",
|
||||
"128mb",
|
||||
}
|
||||
_ALLOWED_FLASH_BEFORE = {"default-reset", "usb-reset", "no-reset", "no-reset-no-sync"}
|
||||
_ALLOWED_FLASH_AFTER = {
|
||||
"default-reset",
|
||||
"hard-reset",
|
||||
"soft-reset",
|
||||
"watchdog-reset",
|
||||
"no-reset",
|
||||
"no-reset-stub",
|
||||
"noreset",
|
||||
"none",
|
||||
"no",
|
||||
}
|
||||
|
||||
|
||||
def _coerce_str(value: object, label: str) -> str:
|
||||
if isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
raise ValueError(f"私有配置无效: {label}")
|
||||
|
||||
|
||||
def _optional_str(value: object) -> Optional[str]:
|
||||
if isinstance(value, str):
|
||||
text = value.strip()
|
||||
return text if text else None
|
||||
return None
|
||||
|
||||
|
||||
def _optional_bool_or_none(value: object) -> Optional[bool]:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, (int, float)):
|
||||
return bool(int(value))
|
||||
if isinstance(value, str):
|
||||
text = value.strip().lower()
|
||||
if text in {"1", "true", "yes", "y", "on"}:
|
||||
return True
|
||||
if text in {"0", "false", "no", "n", "off"}:
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def _coerce_positive_float(value: Optional[float], fallback: float) -> float:
|
||||
if value is None:
|
||||
return fallback
|
||||
try:
|
||||
parsed = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return fallback
|
||||
return max(1.0, parsed)
|
||||
|
||||
|
||||
def _coerce_positive_int(value: Optional[int], fallback: int) -> int:
|
||||
if value is None:
|
||||
return fallback
|
||||
try:
|
||||
parsed = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return fallback
|
||||
return max(1, parsed)
|
||||
|
||||
|
||||
def _ui_text(ui_cfg: dict, key: str, fallback: str) -> str:
|
||||
value = ui_cfg.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
return fallback
|
||||
|
||||
|
||||
def _coerce_offset_int(value: str, label: str) -> int:
|
||||
try:
|
||||
return int(value, 0)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"私有配置无效: {label}") from exc
|
||||
|
||||
|
||||
def _validate_allowed(
|
||||
value: str,
|
||||
label: str,
|
||||
allowed: set[str],
|
||||
*,
|
||||
normalize_dash: bool = False,
|
||||
) -> str:
|
||||
normalized = value.strip().lower()
|
||||
if normalize_dash:
|
||||
normalized = normalized.replace("_", "-")
|
||||
if normalized not in allowed:
|
||||
allowed_text = ", ".join(sorted(allowed))
|
||||
raise ValueError(f"私有配置无效: {label}={value}(允许: {allowed_text})")
|
||||
return normalized
|
||||
|
||||
|
||||
def _validate_app_layout_binding(
|
||||
layout: list[tuple[str, str]],
|
||||
app_bin_name: str,
|
||||
app_partition_offset: str,
|
||||
) -> None:
|
||||
app_offsets = [offset for offset, name in layout if name == app_bin_name]
|
||||
if not app_offsets:
|
||||
raise ValueError("私有配置无效: flash.app_bin_name 必须出现在 flash.layout")
|
||||
|
||||
app_layout_offsets = {_coerce_offset_int(offset, "flash.layout.offset") for offset in app_offsets}
|
||||
if len(app_layout_offsets) != 1:
|
||||
raise ValueError("私有配置无效: flash.layout 中 app_bin_name 对应多个偏移地址")
|
||||
|
||||
app_offset = _coerce_offset_int(app_partition_offset, "flash.app_partition_offset")
|
||||
if app_offset not in app_layout_offsets:
|
||||
layout_offset = app_offsets[0]
|
||||
raise ValueError(
|
||||
"私有配置无效: flash.app_partition_offset 与 flash.layout 中 app_bin_name 的 offset 不一致"
|
||||
f"(app_partition_offset={app_partition_offset}, layout_offset={layout_offset})"
|
||||
)
|
||||
|
||||
|
||||
def _load_flash_private_config(
|
||||
base_dir: Path, raw: Optional[dict] = None
|
||||
) -> FlashPrivateConfig:
|
||||
if raw is None:
|
||||
raw = load_private_config(base_dir)
|
||||
flash_cfg = get_required(raw, ("flash",), (dict,))
|
||||
chip = _coerce_str(get_required(flash_cfg, ("chip",), (str, int)), "flash.chip")
|
||||
baud = _coerce_str(get_required(flash_cfg, ("baud",), (str, int)), "flash.baud")
|
||||
bin_dir_raw = _coerce_str(get_required(flash_cfg, ("bin_dir",), (str,)), "flash.bin_dir")
|
||||
bin_dir = Path(bin_dir_raw)
|
||||
|
||||
bin_enc_raw = get_optional(flash_cfg, ("bin_encryption",), None)
|
||||
if bin_enc_raw is not None:
|
||||
raise ValueError("私有配置无效: flash.bin_encryption 已移除。")
|
||||
|
||||
layout_raw = get_required(flash_cfg, ("layout",), (list,))
|
||||
layout: list[tuple[str, str]] = []
|
||||
for entry in layout_raw:
|
||||
offset: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
if isinstance(entry, (list, tuple)) and len(entry) == 2:
|
||||
offset = _coerce_str(entry[0], "flash.layout.offset")
|
||||
name = _coerce_str(entry[1], "flash.layout.name")
|
||||
elif isinstance(entry, dict):
|
||||
offset = _coerce_str(entry.get("offset"), "flash.layout.offset")
|
||||
name = _coerce_str(entry.get("name"), "flash.layout.name")
|
||||
else:
|
||||
raise ValueError("私有配置无效: flash.layout")
|
||||
if name.endswith(".enc"):
|
||||
raise ValueError("私有配置无效: flash.layout 不应包含 .enc 后缀。")
|
||||
layout.append((offset, name))
|
||||
if not layout:
|
||||
raise ValueError("私有配置无效: flash.layout 不能为空")
|
||||
|
||||
app_bin_name = _coerce_str(
|
||||
get_required(flash_cfg, ("app_bin_name",), (str,)), "flash.app_bin_name"
|
||||
)
|
||||
if app_bin_name.endswith(".enc"):
|
||||
raise ValueError("私有配置无效: flash.app_bin_name 不应包含 .enc 后缀。")
|
||||
app_partition_offset = _coerce_str(
|
||||
get_required(flash_cfg, ("app_partition_offset",), (str, int)),
|
||||
"flash.app_partition_offset",
|
||||
)
|
||||
_validate_app_layout_binding(layout, app_bin_name, app_partition_offset)
|
||||
|
||||
flash_args_raw = get_required(flash_cfg, ("flash_args",), (dict,))
|
||||
use_stub_value = _optional_bool_or_none(get_optional(flash_args_raw, ("use_stub",), None))
|
||||
flash_args = {
|
||||
"flash_mode": _validate_allowed(
|
||||
_coerce_str(
|
||||
get_required(flash_args_raw, ("flash_mode",), (str, int)),
|
||||
"flash.flash_args.flash_mode",
|
||||
),
|
||||
"flash.flash_args.flash_mode",
|
||||
_ALLOWED_FLASH_MODE,
|
||||
),
|
||||
"flash_freq": _validate_allowed(
|
||||
_coerce_str(
|
||||
get_required(flash_args_raw, ("flash_freq",), (str, int)),
|
||||
"flash.flash_args.flash_freq",
|
||||
),
|
||||
"flash.flash_args.flash_freq",
|
||||
_ALLOWED_FLASH_FREQ,
|
||||
),
|
||||
"flash_size": _validate_allowed(
|
||||
_coerce_str(
|
||||
get_required(flash_args_raw, ("flash_size",), (str, int)),
|
||||
"flash.flash_args.flash_size",
|
||||
),
|
||||
"flash.flash_args.flash_size",
|
||||
_ALLOWED_FLASH_SIZE,
|
||||
),
|
||||
"before": _validate_allowed(
|
||||
_coerce_str(
|
||||
get_required(flash_args_raw, ("before",), (str, int)),
|
||||
"flash.flash_args.before",
|
||||
),
|
||||
"flash.flash_args.before",
|
||||
_ALLOWED_FLASH_BEFORE,
|
||||
normalize_dash=True,
|
||||
),
|
||||
"after": _validate_allowed(
|
||||
_coerce_str(
|
||||
get_required(flash_args_raw, ("after",), (str, int)),
|
||||
"flash.flash_args.after",
|
||||
),
|
||||
"flash.flash_args.after",
|
||||
_ALLOWED_FLASH_AFTER,
|
||||
normalize_dash=True,
|
||||
),
|
||||
"use_stub": True if use_stub_value is None else use_stub_value,
|
||||
}
|
||||
|
||||
check_cfg = get_required(raw, ("device_check",), (dict,))
|
||||
crypt_cnt_fields_raw = get_required(check_cfg, ("crypt_cnt_fields",), (list,))
|
||||
crypt_cnt_fields = tuple(_coerce_str(item, "device_check.crypt_cnt_fields") for item in crypt_cnt_fields_raw)
|
||||
crypt_cnt_expect = _optional_str(get_optional(check_cfg, ("crypt_cnt_expect",), None))
|
||||
if crypt_cnt_expect:
|
||||
expect_norm = crypt_cnt_expect.strip().lower()
|
||||
if expect_norm not in {"odd", "even", "zero", "any"}:
|
||||
raise ValueError("私有配置无效: device_check.crypt_cnt_expect")
|
||||
crypt_cnt_expect = expect_norm
|
||||
secure_boot_expected = _optional_bool_or_none(
|
||||
get_optional(check_cfg, ("secure_boot_expected",), None)
|
||||
)
|
||||
secure_boot_field = _coerce_str(
|
||||
get_required(check_cfg, ("secure_boot_field",), (str,)), "device_check.secure_boot_field"
|
||||
)
|
||||
flash_key_purpose_match = _optional_str(
|
||||
get_optional(check_cfg, ("flash_key_purpose_match",), None)
|
||||
)
|
||||
secure_boot_key_purpose_match = _optional_str(
|
||||
get_optional(check_cfg, ("secure_boot_key_purpose_match",), None)
|
||||
)
|
||||
key_read_disable_field = _coerce_str(
|
||||
get_required(check_cfg, ("key_read_disable_field",), (str,)),
|
||||
"device_check.key_read_disable_field",
|
||||
)
|
||||
ui_cfg = get_optional(raw, ("ui",), {})
|
||||
if not isinstance(ui_cfg, dict):
|
||||
ui_cfg = {}
|
||||
return FlashPrivateConfig(
|
||||
chip=chip,
|
||||
baud=baud,
|
||||
bin_dir=bin_dir,
|
||||
layout=layout,
|
||||
app_bin_name=app_bin_name,
|
||||
app_partition_offset=app_partition_offset,
|
||||
flash_args=flash_args,
|
||||
device_check=DeviceCheckConfig(
|
||||
crypt_cnt_fields=crypt_cnt_fields,
|
||||
secure_boot_field=secure_boot_field,
|
||||
secure_boot_expected=secure_boot_expected,
|
||||
crypt_cnt_expect=crypt_cnt_expect,
|
||||
flash_key_purpose_match=flash_key_purpose_match,
|
||||
secure_boot_key_purpose_match=secure_boot_key_purpose_match,
|
||||
key_read_disable_field=key_read_disable_field,
|
||||
),
|
||||
ui=ui_cfg,
|
||||
)
|
||||
Reference in New Issue
Block a user