191 lines
5.7 KiB
Python
191 lines
5.7 KiB
Python
"""Настройки загрузки Raspberry Pi в config.txt (disable_splash и т.д.)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
_DISABLE_SPLASH_RE = re.compile(r"^\s*#?\s*disable_splash\s*=", re.IGNORECASE)
|
|
_SECTION_RE = re.compile(r"^\s*\[(.+)]\s*$")
|
|
|
|
|
|
def _config_candidates() -> List[Path]:
|
|
return [
|
|
Path("/boot/firmware/config.txt"),
|
|
Path("/boot/config.txt"),
|
|
]
|
|
|
|
|
|
def find_config_txt(custom: str = "") -> Optional[Path]:
|
|
if custom:
|
|
p = Path(custom).expanduser()
|
|
return p if p.is_file() else None
|
|
for p in _config_candidates():
|
|
if p.is_file():
|
|
return p
|
|
return None
|
|
|
|
|
|
def _read_text(path: Path) -> str:
|
|
return path.read_text(encoding="utf-8", errors="replace")
|
|
|
|
|
|
def _parse_disable_splash_enabled(text: str) -> bool:
|
|
for line in text.splitlines():
|
|
stripped = line.strip()
|
|
if stripped.startswith("#"):
|
|
continue
|
|
if stripped == "disable_splash=1":
|
|
return True
|
|
return False
|
|
|
|
|
|
def _patch_disable_splash(text: str, enabled: bool) -> str:
|
|
lines = text.splitlines()
|
|
kept: List[str] = []
|
|
for line in lines:
|
|
if _DISABLE_SPLASH_RE.match(line):
|
|
continue
|
|
kept.append(line)
|
|
|
|
if not enabled:
|
|
return "\n".join(kept).rstrip() + "\n"
|
|
|
|
insert_at = len(kept)
|
|
for i, line in enumerate(kept):
|
|
if _SECTION_RE.match(line) and _SECTION_RE.match(line).group(1).strip().lower() == "all":
|
|
insert_at = i + 1
|
|
break
|
|
|
|
kept.insert(insert_at, "disable_splash=1")
|
|
return "\n".join(kept).rstrip() + "\n"
|
|
|
|
|
|
def _write_via_sudo(path: Path, content: str, custom_cmd: str = "") -> Tuple[bool, str]:
|
|
if custom_cmd:
|
|
args = shlex.split(custom_cmd) + [str(path)]
|
|
proc = subprocess.run(
|
|
args,
|
|
input=content,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
if proc.returncode != 0:
|
|
err = (proc.stderr or proc.stdout or "").strip() or f"exit {proc.returncode}"
|
|
return False, err
|
|
return True, "Конфиг обновлён"
|
|
|
|
try:
|
|
if path.is_file() and path.stat().st_mode & 0o200:
|
|
path.write_text(content, encoding="utf-8")
|
|
return True, "Конфиг обновлён"
|
|
except OSError:
|
|
pass
|
|
|
|
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False, suffix=".txt") as tmp:
|
|
tmp.write(content)
|
|
tmp_path = tmp.name
|
|
|
|
try:
|
|
proc = subprocess.run(
|
|
["sudo", "cp", tmp_path, str(path)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
if proc.returncode != 0:
|
|
err = (proc.stderr or proc.stdout or "").strip()
|
|
hint = " Нужен sudo без пароля или WESP_PI_BOOT_CONFIG_WRITE_CMD."
|
|
return False, (err or f"exit {proc.returncode}") + hint
|
|
subprocess.run(["sudo", "sync"], capture_output=True, timeout=30)
|
|
return True, "Конфиг обновлён (sudo)"
|
|
finally:
|
|
try:
|
|
Path(tmp_path).unlink(missing_ok=True)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def get_rainbow_splash_status(config_path: str = "") -> Dict[str, Any]:
|
|
path = find_config_txt(config_path)
|
|
if not path:
|
|
return {
|
|
"config_found": False,
|
|
"config_path": config_path or str(_config_candidates()[0]),
|
|
"disable_splash": False,
|
|
"message": "config.txt не найден (не Raspberry Pi или другой путь)",
|
|
}
|
|
|
|
try:
|
|
text = _read_text(path)
|
|
except OSError as exc:
|
|
return {
|
|
"config_found": True,
|
|
"config_path": str(path),
|
|
"disable_splash": False,
|
|
"readable": False,
|
|
"message": str(exc),
|
|
}
|
|
|
|
enabled = _parse_disable_splash_enabled(text)
|
|
return {
|
|
"config_found": True,
|
|
"config_path": str(path),
|
|
"readable": True,
|
|
"disable_splash": enabled,
|
|
"message": (
|
|
"Радужный splash прошивки отключён (disable_splash=1)"
|
|
if enabled
|
|
else "Радужный splash прошивки включён (стандарт Pi)"
|
|
),
|
|
}
|
|
|
|
|
|
def set_rainbow_splash_disabled(
|
|
enabled: bool,
|
|
*,
|
|
config_path: str = "",
|
|
write_cmd: str = "",
|
|
) -> Dict[str, Any]:
|
|
"""enabled=True → записать disable_splash=1; False → убрать строку."""
|
|
path = find_config_txt(config_path)
|
|
if not path:
|
|
return {
|
|
"ok": False,
|
|
"code": "config_missing",
|
|
"message": "config.txt не найден",
|
|
}
|
|
|
|
try:
|
|
original = _read_text(path)
|
|
except OSError as exc:
|
|
return {"ok": False, "code": "read_error", "message": str(exc)}
|
|
|
|
patched = _patch_disable_splash(original, enabled)
|
|
if patched == original and enabled == _parse_disable_splash_enabled(original):
|
|
return {
|
|
"ok": True,
|
|
"code": "unchanged",
|
|
"message": "Уже в нужном состоянии",
|
|
"disable_splash": enabled,
|
|
"config_path": str(path),
|
|
}
|
|
|
|
ok, msg = _write_via_sudo(path, patched, write_cmd)
|
|
if not ok:
|
|
return {"ok": False, "code": "write_error", "message": msg, "config_path": str(path)}
|
|
|
|
action = "отключён" if enabled else "включён (строка убрана)"
|
|
return {
|
|
"ok": True,
|
|
"code": "updated",
|
|
"message": f"Радужный splash прошивки {action}.",
|
|
"disable_splash": enabled,
|
|
"config_path": str(path),
|
|
}
|