Improve controller behavior and update project requirements
This commit is contained in:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user