@@ -0,0 +1,190 @@
|
||||
"""Настройки загрузки 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),
|
||||
}
|
||||
Reference in New Issue
Block a user