feat(factory): add local flashing toolkit and secure OTA defaults

This commit is contained in:
admin
2026-03-02 01:57:39 +08:00
parent 809fcf6548
commit 3d3c3ebac2
27 changed files with 5260 additions and 4 deletions

View 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,
)