初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user