Improve controller behavior and update project requirements

This commit is contained in:
2026-09-30 15:42:36 +08:00
parent 55374356ed
commit 071f06d365
52 changed files with 2205 additions and 335 deletions
+54 -16
View File
@@ -9,12 +9,14 @@ from io import BytesIO
from pathlib import Path
from uuid import UUID, uuid4
from contextlib import nullcontext
from PIL import Image
from app.persistence import atomic_write_bytes
from app.demo_library import demo_template, demo_animation
from app.display.service import ANIMATION_PLAYBACK_SPEEDS, AnimationPlaybackConflictError
from app.templates.store import TemplateConflictError, TemplateNotFoundError
from .session import RpcError, checked_name
from .preview_cache import PreviewCache
SETTINGS = ("brightness", "orientation", "matrix_refresh_rate_limit_hz", "performance_mode_enabled")
@@ -30,6 +32,7 @@ class MobileControl:
self.tasks = {}
self.library_order = library_order
self.path = Path(control.store.data_dir) / "mobile/identity.json"
self.preview_cache = PreviewCache(control.store.data_dir)
if self.path.exists():
identity = json.loads(self.path.read_text(encoding="utf-8"))
if set(identity) != {"schema", "device_id", "name"} or identity["schema"] != 1:
@@ -40,6 +43,7 @@ class MobileControl:
else:
self.identity_record = dict(schema=1, device_id=str(uuid4()), name="奇妙小屏幕")
self._persist_identity(self.identity_record)
self.preview_cache.reconcile(self._library_items())
def _persist_identity(self, identity):
atomic_write_bytes(self.path, (json.dumps(identity, ensure_ascii=False) + "\n").encode("utf-8"))
@@ -47,8 +51,8 @@ class MobileControl:
def identity(self):
record = self.identity_record
return dict(device_id=record["device_id"], device_name=record["name"],
short_id=record["device_id"].replace("-", "")[:8], protocol_major=1, protocol_minor=0,
capabilities=["status", "settings", "library", "playback", "frame", "device_name", "wifi", "wifi_scan"],
short_id=record["device_id"].replace("-", "")[:8], protocol_major=1, protocol_minor=1,
capabilities=["status", "settings", "library", "library_progressive", "playback", "frame", "device_name", "wifi", "wifi_scan"],
limits=dict(max_message_bytes=65536, library_page_size=50, preview_interval_ms=2000))
def settings(self):
@@ -66,6 +70,42 @@ class MobileControl:
raise RpcError("CONFLICT", "内容已改变,请刷新")
return resolved
@staticmethod
def _preview_image(content):
if content["type"] == "template":
return content["image"]
frames = content["frames"]
if not frames:
raise RpcError("NOT_FOUND", "内容没有可预览帧")
frame = frames[0]
if "image" in frame:
return frame["image"]
if "path" not in frame:
raise RpcError("NOT_FOUND", "内容没有可预览帧")
try:
with Image.open(frame["path"]) as image:
image.load()
if image.size != (64, 64):
raise ValueError("invalid animation frame size")
return image.convert("RGB")
except (OSError, ValueError):
raise RpcError("NOT_FOUND", "预览文件不可用") from None
def _library_items(self):
items = []
for kind, records in (("template", [demo_template()] + self.templates.list()["templates"]),
("animation", [demo_animation()] + self.animations.list()["animations"])):
for item in records:
items.append(dict(type=kind, id=item["id"], name=item["name"], revision=item["revision"],
is_demo=bool(item.get("is_demo", False)), playable=kind == "template" or item.get("frame_count", 0) > 0,
thumbnail_revision=item["revision"]))
if self.library_order is not None:
user_items = [item for item in items if not item["is_demo"]]
order = self.library_order.get([{"type": item["type"], "id": item["id"]} for item in user_items])["items"]
by_key = {(item["type"], item["id"]): item for item in user_items}
items = [item for item in items if item["is_demo"]] + [by_key[(ref["type"], ref["id"])] for ref in order]
return items
@staticmethod
def _png(image):
output = BytesIO()
@@ -145,20 +185,12 @@ class MobileControl:
else:
self.control.update_config(changes)
return self.settings()
if method == "library.list":
items = []
for kind, records in (("template", [demo_template()] + self.templates.list()["templates"]),
("animation", [demo_animation()] + self.animations.list()["animations"])):
for item in records:
items.append(dict(type=kind, id=item["id"], name=item["name"], revision=item["revision"],
is_demo=bool(item.get("is_demo", False)), playable=kind == "template" or item.get("frame_count", 0) > 0,
thumbnail_revision=item["revision"]))
if self.library_order is not None:
user_items = [item for item in items if not item['is_demo']]
order = self.library_order.get([{'type': item['type'], 'id': item['id']} for item in user_items])['items']
by_key = {(item['type'], item['id']): item for item in user_items}
items = [item for item in items if item['is_demo']] + [by_key[(ref['type'], ref['id'])] for ref in order]
if method in ("library.list", "library.summary"):
items = self._library_items()
revision = self.control.revision(items)
self.preview_cache.reconcile(items)
if method == "library.summary":
return dict(library_revision=revision, item_count=len(items))
limit = params.get("limit", 20)
if type(limit) is not int or not 1 <= limit <= 50:
raise ValueError()
@@ -175,7 +207,13 @@ class MobileControl:
next_cursor=f"{revision}:{next_offset}" if next_offset < len(items) else None)
if method == "library.thumbnail":
content = self._content(params)
return self._png(content["image"] if content["type"] == "template" else content["frames"][0]["image"])
return self._png(self._preview_image(content))
if method == "library.preview":
content = self._content(params)
image = self._preview_image(content)
item = {key: content[key] for key in ("type", "id", "revision")}
data = self.preview_cache.get(item, image)
return dict(mime="image/png", data_base64=base64.b64encode(data).decode("ascii"))
if method in ("content.play", "content.default.set"):
content = self._content(params)
if method == "content.default.set":