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

133 lines
4.4 KiB
Python

"""Hold a real-driver visual pattern until systemd or the operator stops it."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import signal
import time
from PIL import Image
from app.display.matrix_driver import MatrixDisplayDriver
from app.display.ota_indicator import render_ota_indicator
from app.display.transforms import apply_orientation, apply_row_bit_order, get_row_bit_order
from app.display.wifi_indicator import (
WIFI_SCROLL_FPS,
WIFI_SCROLL_GAP_PIXELS,
WIFI_SCROLL_PIXELS_PER_SECOND,
render_wifi_indicator,
text_width,
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--brightness", type=int, default=50)
parser.add_argument("--mode", choices=("wifi", "complex", "ota"), default="wifi")
parser.add_argument(
"--message",
default="SSID: DRIVER-TEST IP: http://192.168.1.2:8080/",
)
parser.add_argument("--progress-file")
parser.add_argument("--orientation", type=int, choices=(0, 90, 180, 270), default=0)
args = parser.parse_args()
if not 1 <= args.brightness <= 100:
parser.error("brightness must be in 1..100")
stopping = False
def request_stop(_signum: int, _frame: object) -> None:
nonlocal stopping
stopping = True
signal.signal(signal.SIGINT, request_stop)
signal.signal(signal.SIGTERM, request_stop)
driver = MatrixDisplayDriver(args.brightness, limit_refresh_rate_hz=100)
cycle_pixels = (
64 + text_width(args.message) + WIFI_SCROLL_GAP_PIXELS
if args.mode == "wifi"
else 192
)
started = time.monotonic()
next_frame = started
try:
while not stopping:
now = time.monotonic()
offset = ((now - started) * WIFI_SCROLL_PIXELS_PER_SECOND) % cycle_pixels
if args.mode == "wifi":
frame = render_wifi_indicator(
args.message,
offset=offset,
connected=True,
)
elif args.mode == "complex":
frame = render_complex_pattern(offset)
else:
percent, stage = read_ota_progress(args.progress_file)
frame = render_ota_indicator(percent, f"{stage}:{int(offset) % 4}")
transformed = apply_row_bit_order(
apply_orientation(frame, args.orientation),
get_row_bit_order(),
)
driver.set_image(transformed)
next_frame += 1.0 / WIFI_SCROLL_FPS
time.sleep(max(0.0, next_frame - time.monotonic()))
finally:
driver.clear()
driver.close()
return 0
def read_ota_progress(filename: str | None) -> tuple[int, str]:
if not filename:
return 0, "准备更新"
try:
document = json.loads(Path(filename).read_text(encoding="utf-8"))
job = document.get("job") if isinstance(document, dict) else None
if not isinstance(job, dict):
return 0, "准备更新"
return max(0, min(100, int(job.get("percent", 0)))), str(job.get("message") or "更新中")[:80]
except (OSError, UnicodeError, json.JSONDecodeError, TypeError, ValueError):
return 0, "准备更新"
def render_complex_pattern(offset: float) -> Image.Image:
"""Render a dense, high-contrast 192-pixel panorama through a 64px window."""
phase = int(offset) % 192
pixels: list[tuple[int, int, int]] = []
palette = (
(255, 32, 32),
(255, 176, 24),
(48, 255, 64),
(32, 224, 255),
(48, 64, 255),
(224, 48, 255),
)
for y in range(64):
for x in range(64):
source_x = (x + phase) % 192
base = palette[(source_x // 16) % len(palette)]
checker = ((source_x // 4) ^ (y // 4)) & 1
diagonal = ((source_x + y * 2) // 8) & 1
gain = 1.0 if checker else 0.48
if diagonal:
gain *= 0.78
radial = ((source_x % 32) - 16) ** 2 + (y - 32) ** 2
if 70 <= radial <= 150:
color = (255, 255, 255)
elif radial < 36:
color = (8, 8, 8)
else:
color = tuple(max(8, min(255, int(channel * gain))) for channel in base)
pixels.append(color)
image = Image.new("RGB", (64, 64))
image.putdata(pixels)
return image
if __name__ == "__main__":
raise SystemExit(main())