Files
matrix-screen-controller/核桃派软件源代码/scripts/axp313a_kernel_health.py
T

183 lines
7.2 KiB
Python

from __future__ import annotations
import argparse
import json
import os
import platform
import subprocess
import time
import urllib.request
from pathlib import Path
from typing import Any
CANDIDATE_RELEASE = "6.1.31-matrix-axp313a1"
MARKER_PATH = Path("/boot/matrix-kernel-good")
CPUFREQ_ROOT = Path("/sys/devices/system/cpu/cpufreq")
class HealthError(RuntimeError):
pass
def _read(path: Path) -> str:
return path.read_text(encoding="ascii", errors="strict").strip()
def check_axp313a_binding(sys_root: Path = Path("/sys")) -> None:
matches = list((sys_root / "bus/i2c/devices").glob("*-0036/of_node/compatible"))
if not matches:
raise HealthError("PMIC device-tree node at I2C address 0x36 is missing")
compatible = matches[0].read_bytes().replace(b"\0", b"\n").decode("ascii", errors="replace")
if "x-powers,axp313a" not in compatible.splitlines():
raise HealthError(f"PMIC compatible is not x-powers,axp313a: {compatible!r}")
driver_link = matches[0].parents[1] / "driver"
if not driver_link.exists() or Path(os.path.realpath(driver_link)).name != "axp20x-i2c":
raise HealthError("AXP313A is not bound to axp20x-i2c")
def check_cpufreq(
cpufreq_root: Path = CPUFREQ_ROOT,
debugfs_deferred: Path = Path("/sys/kernel/debug/devices_deferred"),
) -> None:
policy = cpufreq_root / "policy0"
if not policy.is_dir():
raise HealthError("cpufreq policy0 is missing")
affected = set(_read(policy / "affected_cpus").split())
if affected != {"0", "1", "2", "3"}:
raise HealthError(f"policy0 does not cover CPU0-3: {sorted(affected)}")
available = set(_read(policy / "scaling_available_governors").split())
if "performance" not in available:
raise HealthError("performance governor is unavailable")
if debugfs_deferred.is_file() and "cpufreq-dt" in debugfs_deferred.read_text(
encoding="utf-8", errors="replace"
):
raise HealthError("cpufreq-dt is still deferred")
def check_cpu_regulator(sys_root: Path = Path("/sys")) -> None:
regulator_root = sys_root / "class/regulator"
for directory in regulator_root.glob("regulator.*"):
name_path = directory / "name"
if not name_path.is_file() or _read(name_path) != "vdd-cpu":
continue
voltage = int(_read(directory / "microvolts"))
if not 810_000 <= voltage <= 1_080_000:
raise HealthError(f"vdd-cpu is outside the approved range: {voltage}uV")
return
raise HealthError("vdd-cpu regulator is missing")
def check_kernel_messages(messages: str) -> None:
forbidden = (
"DCDC frequency on AXP313a is fixed to 3 MHz",
"Error setting dcdc frequency",
)
for message in forbidden:
if message in messages:
raise HealthError(f"AXP313A regulator logged a boot error: {message}")
def check_kernel_log() -> None:
result = subprocess.run(
["/bin/dmesg"],
check=True,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
check_kernel_messages(result.stdout)
def _fetch_status(url: str) -> dict[str, Any]:
# The candidate kernel is first validated against the already-installed
# production application. That older build can take several seconds to
# answer while its display compositor is busy, which must not masquerade
# as a kernel/cpufreq failure. The stricter 500 ms HTTP performance gate
# is measured separately after the candidate application is staged.
with urllib.request.urlopen(url, timeout=10.0) as response:
return json.loads(response.read().decode("utf-8"))
def check_driver_pair(first: dict[str, Any], second: dict[str, Any]) -> None:
screen = second.get("screen") or {}
if screen.get("driver") != "walnutpi-h618-hub75":
raise HealthError("production H618 driver is not active")
driver = screen.get("driver_status") or {}
previous = (first.get("screen") or {}).get("driver_status") or {}
for field in ("actual_refresh_rate_hz", "panel_scan_rate_hz"):
if float(driver.get(field) or 0.0) < 95.0:
raise HealthError(f"{field} is below 95Hz")
if driver.get("scans_per_frame") != 1:
raise HealthError("scans_per_frame is not 1")
frame_delta = int(driver.get("completed_frames") or 0) - int(previous.get("completed_frames") or 0)
scan_delta = int(driver.get("completed_scans") or 0) - int(previous.get("completed_scans") or 0)
miss_delta = int(driver.get("deadline_misses") or 0) - int(previous.get("deadline_misses") or 0)
if frame_delta <= 0 or scan_delta != frame_delta:
raise HealthError("HUB75 completed frame/scan counters are stalled or unequal")
if miss_delta < 0 or miss_delta / frame_delta > 0.001:
raise HealthError("HUB75 deadline miss rate exceeds 0.1%")
for field in ("oe_pulse_faults", "oe_forced_blanks"):
if int(driver.get(field) or 0) != int(previous.get(field) or 0):
raise HealthError(f"{field} increased during health check")
for field in ("driver_error", "oe_timing_error", "safeoff_error"):
if driver.get(field):
raise HealthError(f"{field} is not empty: {driver[field]}")
if driver.get("cpu_affinity") != 3 or driver.get("cpu_affinity_active") is not True:
raise HealthError("CPU3 affinity is not active")
if driver.get("realtime_priority") != 50 or driver.get("realtime_priority_active") is not True:
raise HealthError("SCHED_FIFO 50 is not active")
if driver.get("memory_locked") is not True:
raise HealthError("refresh memory is not locked")
def wait_for_driver(url: str, timeout_seconds: int = 60) -> None:
deadline = time.monotonic() + timeout_seconds
last_error: Exception | None = None
while time.monotonic() < deadline:
try:
first = _fetch_status(url)
time.sleep(2)
second = _fetch_status(url)
check_driver_pair(first, second)
return
except Exception as exc:
last_error = exc
time.sleep(1)
raise HealthError(f"application/GPIO health timed out: {last_error}")
def write_marker(marker: Path = MARKER_PATH) -> None:
temporary = marker.with_name(f".{marker.name}.tmp")
with temporary.open("w", encoding="ascii", newline="\n") as handle:
handle.write(f"{CANDIDATE_RELEASE}\n")
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, marker)
os.sync()
def main() -> int:
parser = argparse.ArgumentParser(description="Confirm the one-shot WalnutPi AXP313A kernel")
parser.add_argument("--status-url", default="http://127.0.0.1:8080/api/status")
parser.add_argument("--marker", type=Path, default=MARKER_PATH)
args = parser.parse_args()
if platform.release() != CANDIDATE_RELEASE:
raise HealthError(f"unexpected candidate release: {platform.release()}")
check_axp313a_binding()
check_cpufreq()
check_cpu_regulator()
check_kernel_log()
wait_for_driver(args.status_url)
write_marker(args.marker)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except HealthError as exc:
print(f"AXP313A candidate health failed: {exc}", flush=True)
raise SystemExit(1)