commit 108f08645cc2cd5e6a9792ff6c0fcc440ad839a8 Author: space Date: Sun May 10 12:46:33 2026 +0200 initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ff4cb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Python +__pycache__/ +*.py[cod] +*.pyd +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +.coverage.* +.venv/ +venv/ +env/ + +# Local app data +data/ + +# Frontend +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ +frontend/*.tsbuildinfo + +# Logs +*.log +!button_log.txt + +# OS / editors +.DS_Store +Thumbs.db diff --git a/Box.f3d b/Box.f3d new file mode 100644 index 0000000..e8fe44b Binary files /dev/null and b/Box.f3d differ diff --git a/Insert (first test).f3d b/Insert (first test).f3d new file mode 100644 index 0000000..ce1dfc6 Binary files /dev/null and b/Insert (first test).f3d differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff5e71d --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# Custom Streamdeck Button Prototype + +This project is now a local Stream Deck-style control app for the 10-button Pico prototype. + +The Pico firmware still prints the same USB serial JSON events. The new backend consumes those events, stores configuration in SQLite, executes actions, loads backend plugins, and serves the built React UI. + +## Button Wiring + +Button order is the order you gave: + +1. GP28 +2. GP27 +3. GP26 +4. GP22 +5. GP21 +6. GP20 +7. GP18 +8. GP19 +9. GP17 +10. GP16 + +The other leg of every button goes to GND. The Pico script uses internal pull-ups, so a pressed button reads as LOW. + +## Files + +- `pico/main.py`: MicroPython code that runs on the Pico and prints button events over USB serial. +- `pc/listen_buttons.py`: PC listener that reads serial events and appends them to `button_log.txt`. +- `backend/`: FastAPI backend, WebSocket events, SQLite state, action engine, app discovery, plugin loading. +- `frontend/`: React + Vite + Tailwind UI. +- `plugins/`: Backend Python plugins. Each plugin exposes a top-level `PLUGIN` object. + +## Run The App + +Install Python dependencies: + +```powershell +python -m pip install -r requirements.txt +``` + +Install and build the UI: + +```powershell +cd frontend +npm install +npm run build +cd .. +``` + +Start the backend and open `http://127.0.0.1:8000/`: + +```powershell +python -m uvicorn backend.main:app --host 127.0.0.1 --port 8000 +``` + +For frontend dev mode, run the backend above and then: + +```powershell +cd frontend +npm run dev +``` + +## Legacy Listener + +```powershell +python .\pc\listen_buttons.py +``` + +If auto-detect fails: + +```powershell +python .\pc\listen_buttons.py --port COM5 +``` + +## Plugin Shape + +Backend plugins live in `plugins/` and expose `PLUGIN`. + +```python +class MyPlugin: + name = "My Plugin" + desc = "Does a thing" + actions = [ + { + "id": "do_thing", + "name": "Do Thing", + "fields": [{"id": "value", "label": "Value", "type": "text"}], + } + ] + + def on_load(self, ctx): + pass + + def on_event(self, ctx, event): + pass + + def execute_action(self, ctx, action_id, config, event): + pass + +PLUGIN = MyPlugin() +``` diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..f716bc1 --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1,2 @@ +"""Custom Streamdeck backend package.""" + diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..76b31a3 --- /dev/null +++ b/backend/database.py @@ -0,0 +1,366 @@ +from __future__ import annotations + +import json +import sqlite3 +import uuid +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parent.parent +DEFAULT_DB = ROOT / "data" / "streamdeck.sqlite" + + +def utc_now() -> str: + return datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z") + + +class Database: + def __init__(self, path: Path = DEFAULT_DB): + self.path = path + self.path.parent.mkdir(parents=True, exist_ok=True) + self.conn = sqlite3.connect(self.path, check_same_thread=False) + self.conn.row_factory = sqlite3.Row + self.conn.execute("PRAGMA foreign_keys = ON") + self.migrate() + self.ensure_defaults() + + def migrate(self) -> None: + self.conn.executescript( + """ + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS profiles ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS folders ( + id TEXT PRIMARY KEY, + profile_id TEXT NOT NULL, + parent_id TEXT, + name TEXT NOT NULL, + is_root INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE, + FOREIGN KEY(parent_id) REFERENCES folders(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS buttons ( + id TEXT PRIMARY KEY, + profile_id TEXT NOT NULL, + folder_id TEXT NOT NULL, + position INTEGER NOT NULL, + physical_button INTEGER NOT NULL, + label TEXT NOT NULL DEFAULT '', + color TEXT NOT NULL DEFAULT '#111827', + icon TEXT NOT NULL DEFAULT '', + trigger_mode TEXT NOT NULL DEFAULT 'down', + action_type TEXT NOT NULL DEFAULT 'noop', + action_config TEXT NOT NULL DEFAULT '{}', + updated_at TEXT NOT NULL, + UNIQUE(folder_id, position), + UNIQUE(folder_id, physical_button), + FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE, + FOREIGN KEY(folder_id) REFERENCES folders(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS manual_apps ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + path TEXT NOT NULL, + args TEXT, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS event_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TEXT NOT NULL, + event_type TEXT NOT NULL, + payload TEXT NOT NULL + ); + """ + ) + self.conn.commit() + + def ensure_defaults(self) -> None: + if not self.get_setting("click_check"): + self.set_setting("click_check", False) + if not self.get_setting("serial_port"): + self.set_setting("serial_port", None) + profile = self.conn.execute("SELECT * FROM profiles LIMIT 1").fetchone() + if profile is None: + profile_id = self.create_profile("Default", make_active=False) + self.set_setting("active_profile_id", profile_id) + root_id = self.get_root_folder(profile_id)["id"] + self.set_setting("active_folder_id", root_id) + else: + active_profile = self.get_setting("active_profile_id") or profile["id"] + self.set_setting("active_profile_id", active_profile) + active_folder = self.get_setting("active_folder_id") or self.get_root_folder(active_profile)["id"] + self.set_setting("active_folder_id", active_folder) + self.sync_physical_layouts() + + def row_to_dict(self, row: sqlite3.Row | None) -> dict[str, Any] | None: + return dict(row) if row else None + + def get_setting(self, key: str) -> Any: + row = self.conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone() + return json.loads(row["value"]) if row else None + + def set_setting(self, key: str, value: Any) -> None: + self.conn.execute( + "INSERT INTO settings(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", + (key, json.dumps(value)), + ) + self.conn.commit() + + def settings(self) -> dict[str, Any]: + rows = self.conn.execute("SELECT key, value FROM settings").fetchall() + return {row["key"]: json.loads(row["value"]) for row in rows} + + def create_profile(self, name: str, make_active: bool = True) -> str: + profile_id = str(uuid.uuid4()) + self.conn.execute( + "INSERT INTO profiles(id, name, created_at) VALUES(?, ?, ?)", + (profile_id, name, utc_now()), + ) + root_id = str(uuid.uuid4()) + self.conn.execute( + "INSERT INTO folders(id, profile_id, parent_id, name, is_root, created_at) VALUES(?, ?, NULL, ?, 1, ?)", + (root_id, profile_id, "Root", utc_now()), + ) + self._seed_buttons(profile_id, root_id) + self.conn.commit() + self.sync_physical_layouts() + if make_active: + self.set_setting("active_profile_id", profile_id) + self.set_setting("active_folder_id", root_id) + return profile_id + + def update_profile(self, profile_id: str, name: str | None = None, active: bool | None = None) -> None: + if name is not None: + self.conn.execute("UPDATE profiles SET name = ? WHERE id = ?", (name, profile_id)) + if active: + self.set_setting("active_profile_id", profile_id) + self.set_setting("active_folder_id", self.get_root_folder(profile_id)["id"]) + self.conn.commit() + + def delete_profile(self, profile_id: str) -> None: + if profile_id == self.first_profile()["id"]: + raise ValueError("Cannot delete the first profile because it owns the hardware layout.") + count = self.conn.execute("SELECT COUNT(*) AS count FROM profiles").fetchone()["count"] + if count <= 1: + raise ValueError("Cannot delete the only profile.") + self.conn.execute("DELETE FROM profiles WHERE id = ?", (profile_id,)) + if self.get_setting("active_profile_id") == profile_id: + next_profile = self.conn.execute("SELECT id FROM profiles LIMIT 1").fetchone()["id"] + self.set_setting("active_profile_id", next_profile) + self.set_setting("active_folder_id", self.get_root_folder(next_profile)["id"]) + self.conn.commit() + + def create_folder(self, profile_id: str, parent_id: str | None, name: str) -> str: + parent_id = parent_id or self.get_root_folder(profile_id)["id"] + folder_id = str(uuid.uuid4()) + self.conn.execute( + "INSERT INTO folders(id, profile_id, parent_id, name, is_root, created_at) VALUES(?, ?, ?, ?, 0, ?)", + (folder_id, profile_id, parent_id, name, utc_now()), + ) + self._seed_buttons(profile_id, folder_id) + self.conn.commit() + self.sync_physical_layouts() + return folder_id + + def update_folder(self, folder_id: str, name: str | None = None, parent_id: str | None = None) -> None: + if name is not None: + self.conn.execute("UPDATE folders SET name = ? WHERE id = ?", (name, folder_id)) + if parent_id is not None: + self.conn.execute("UPDATE folders SET parent_id = ? WHERE id = ? AND is_root = 0", (parent_id, folder_id)) + self.conn.commit() + + def delete_folder(self, folder_id: str) -> None: + row = self.conn.execute("SELECT is_root, profile_id FROM folders WHERE id = ?", (folder_id,)).fetchone() + if not row: + return + if row["is_root"]: + raise ValueError("Cannot delete the root folder.") + self.conn.execute("DELETE FROM folders WHERE id = ?", (folder_id,)) + if self.get_setting("active_folder_id") == folder_id: + self.set_setting("active_folder_id", self.get_root_folder(row["profile_id"])["id"]) + self.conn.commit() + + def update_button(self, button_id: str, changes: dict[str, Any]) -> dict[str, Any]: + physical_button = changes.pop("physical_button", None) + current = self.get_button(button_id) + if physical_button is not None and int(physical_button) != current["physical_button"]: + if not self.can_edit_physical_layout(current["folder_id"]): + raise ValueError("Physical button layout can only be changed in the first profile's root folder.") + allowed = {"label", "color", "icon", "trigger_mode", "action_type"} + sets: list[str] = [] + values: list[Any] = [] + for key in allowed: + if key in changes and changes[key] is not None: + sets.append(f"{key} = ?") + values.append(changes[key]) + if "action_config" in changes and changes["action_config"] is not None: + sets.append("action_config = ?") + values.append(json.dumps(changes["action_config"])) + if sets: + sets.append("updated_at = ?") + values.append(utc_now()) + values.append(button_id) + self.conn.execute(f"UPDATE buttons SET {', '.join(sets)} WHERE id = ?", values) + self.conn.commit() + if physical_button is not None and int(physical_button) != current["physical_button"]: + self.move_physical_button(current["folder_id"], current["position"], int(physical_button)) + return self.get_button(button_id) + + def move_physical_button(self, folder_id: str, position: int, physical_button: int) -> None: + if not self.can_edit_physical_layout(folder_id): + raise ValueError("Physical button layout can only be changed in the first profile's root folder.") + if position < 1 or position > 10 or physical_button < 1 or physical_button > 10: + raise ValueError("Position and physical button must be between 1 and 10.") + + mapping = self.physical_layout_mapping() + old_physical = mapping[position] + old_position = next((slot for slot, physical in mapping.items() if physical == physical_button), None) + mapping[position] = physical_button + if old_position and old_position != position: + mapping[old_position] = old_physical + self.sync_physical_layouts(mapping) + + def get_button(self, button_id: str) -> dict[str, Any]: + row = self.conn.execute("SELECT * FROM buttons WHERE id = ?", (button_id,)).fetchone() + button = dict(row) + button["action_config"] = json.loads(button["action_config"]) + return button + + def find_button_for_event(self, profile_id: str, folder_id: str, physical_button: int) -> dict[str, Any] | None: + row = self.conn.execute( + "SELECT * FROM buttons WHERE profile_id = ? AND folder_id = ? AND physical_button = ?", + (profile_id, folder_id, physical_button), + ).fetchone() + if not row: + return None + button = dict(row) + button["action_config"] = json.loads(button["action_config"]) + return button + + def add_manual_app(self, name: str, path: str, args: str | None = None) -> dict[str, Any]: + app_id = str(uuid.uuid4()) + self.conn.execute( + "INSERT INTO manual_apps(id, name, path, args, created_at) VALUES(?, ?, ?, ?, ?)", + (app_id, name, path, args, utc_now()), + ) + self.conn.commit() + return {"id": app_id, "name": name, "path": path, "args": args, "source": "manual"} + + def manual_apps(self) -> list[dict[str, Any]]: + return [dict(row) | {"source": "manual"} for row in self.conn.execute("SELECT * FROM manual_apps ORDER BY name").fetchall()] + + def add_event(self, event_type: str, payload: dict[str, Any]) -> None: + self.conn.execute( + "INSERT INTO event_history(created_at, event_type, payload) VALUES(?, ?, ?)", + (utc_now(), event_type, json.dumps(payload)), + ) + self.conn.commit() + + def state(self) -> dict[str, Any]: + settings = self.settings() + profiles = [dict(row) for row in self.conn.execute("SELECT * FROM profiles ORDER BY created_at").fetchall()] + folders = [dict(row) for row in self.conn.execute("SELECT * FROM folders ORDER BY is_root DESC, created_at").fetchall()] + buttons = [] + for row in self.conn.execute("SELECT * FROM buttons ORDER BY folder_id, position").fetchall(): + button = dict(row) + button["action_config"] = json.loads(button["action_config"]) + buttons.append(button) + return { + "settings": settings, + "profiles": profiles, + "folders": folders, + "buttons": buttons, + "layout": { + "canonical_profile_id": self.first_profile()["id"], + "canonical_folder_id": self.canonical_layout_folder()["id"], + "mapping": self.physical_layout_mapping(), + }, + } + + def get_root_folder(self, profile_id: str) -> dict[str, Any]: + row = self.conn.execute("SELECT * FROM folders WHERE profile_id = ? AND is_root = 1", (profile_id,)).fetchone() + return dict(row) + + def first_profile(self) -> dict[str, Any]: + row = self.conn.execute("SELECT * FROM profiles ORDER BY created_at LIMIT 1").fetchone() + if row is None: + raise ValueError("No profiles exist.") + return dict(row) + + def canonical_layout_folder(self) -> dict[str, Any]: + return self.get_root_folder(self.first_profile()["id"]) + + def can_edit_physical_layout(self, folder_id: str) -> bool: + return folder_id == self.canonical_layout_folder()["id"] + + def physical_layout_mapping(self) -> dict[int, int]: + folder_id = self.canonical_layout_folder()["id"] + rows = self.conn.execute( + "SELECT position, physical_button FROM buttons WHERE folder_id = ? ORDER BY position", + (folder_id,), + ).fetchall() + mapping = {int(row["position"]): int(row["physical_button"]) for row in rows} + for position in range(1, 11): + mapping.setdefault(position, position) + return mapping + + def sync_physical_layouts(self, mapping: dict[int, int] | None = None) -> None: + if self.conn.execute("SELECT COUNT(*) AS count FROM profiles").fetchone()["count"] == 0: + return + mapping = mapping or self.physical_layout_mapping() + now = utc_now() + folders = self.conn.execute("SELECT id FROM folders").fetchall() + for folder in folders: + folder_id = folder["id"] + for position in range(1, 11): + self.conn.execute( + "UPDATE buttons SET physical_button = ?, updated_at = ? WHERE folder_id = ? AND position = ?", + (-position, now, folder_id, position), + ) + for position, physical_button in mapping.items(): + self.conn.execute( + "UPDATE buttons SET physical_button = ?, updated_at = ? WHERE folder_id = ? AND position = ?", + (physical_button, now, folder_id, position), + ) + self.conn.commit() + + def next_folder_id(self, profile_id: str, current_folder_id: str, direction: str = "next") -> str | None: + rows = self.conn.execute( + "SELECT id FROM folders WHERE profile_id = ? ORDER BY is_root DESC, created_at, name", + (profile_id,), + ).fetchall() + folder_ids = [row["id"] for row in rows] + if not folder_ids: + return None + if current_folder_id not in folder_ids: + return folder_ids[0] + offset = -1 if direction == "previous" else 1 + current_index = folder_ids.index(current_folder_id) + return folder_ids[(current_index + offset) % len(folder_ids)] + + def _seed_buttons(self, profile_id: str, folder_id: str) -> None: + now = utc_now() + for position in range(1, 11): + self.conn.execute( + """ + INSERT INTO buttons( + id, profile_id, folder_id, position, physical_button, label, + color, icon, trigger_mode, action_type, action_config, updated_at + ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, 'down', 'noop', '{}', ?) + """, + (str(uuid.uuid4()), profile_id, folder_id, position, position, f"Button {position}", "#111827", "", now), + ) diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..c94e58a --- /dev/null +++ b/backend/main.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +from contextlib import asynccontextmanager +from pathlib import Path +from typing import Any + +from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles + +from backend.database import Database +from backend.models import ( + ActionTest, + ButtonUpdate, + FolderCreate, + FolderUpdate, + ManualAppCreate, + ProfileCreate, + ProfileUpdate, + SettingsUpdate, + WebSocketCommand, +) +from backend.services.actions import ActionEngine +from backend.services.apps import AppDiscovery +from backend.services.plugins import PluginContext, PluginManager +from backend.services.serial_service import SerialService +from backend.services.websocket_manager import WebSocketManager + + +ROOT = Path(__file__).resolve().parent.parent +FRONTEND_DIST = ROOT / "frontend" / "dist" +PLUGINS_ROOT = ROOT / "plugins" + + +class Runtime: + def __init__(self) -> None: + self.db = Database() + self.ws = WebSocketManager() + self.plugins = PluginManager(PLUGINS_ROOT) + self.actions = ActionEngine(self) + self.serial = SerialService(self) + self.apps = AppDiscovery(self.db) + + def load_plugins(self) -> None: + self.plugins.load_all(PluginContext(self)) + + def public_state(self) -> dict[str, Any]: + state = self.db.state() + state["plugins"] = self.plugins.public_plugins() + state["apps"] = self.apps.discover() + state["device"] = {"connected_port": self.serial.connected_port} + return state + + +runtime = Runtime() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + runtime.load_plugins() + runtime.serial.start() + await runtime.ws.broadcast("plugins.loaded", {"plugins": runtime.plugins.public_plugins()}) + try: + yield + finally: + await runtime.serial.stop() + + +app = FastAPI(title="Custom Streamdeck", lifespan=lifespan) +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/api/state") +def get_state() -> dict[str, Any]: + return runtime.public_state() + + +@app.put("/api/settings") +async def update_settings(payload: SettingsUpdate) -> dict[str, Any]: + restart_serial = False + for key, value in payload.model_dump(exclude_unset=True).items(): + if value is not None: + runtime.db.set_setting(key, value) + if key == "serial_port": + restart_serial = True + if restart_serial: + await runtime.serial.restart() + state = runtime.public_state() + await runtime.ws.broadcast("state.updated", state) + return state + + +@app.get("/api/apps") +def get_apps() -> list[dict[str, Any]]: + return runtime.apps.discover() + + +@app.post("/api/apps/manual") +async def add_manual_app(payload: ManualAppCreate) -> dict[str, Any]: + app_entry = runtime.db.add_manual_app(payload.name, payload.path, payload.args) + await runtime.ws.broadcast("state.updated", runtime.public_state()) + return app_entry + + +@app.get("/api/plugins") +def get_plugins() -> list[dict[str, Any]]: + return runtime.plugins.public_plugins() + + +@app.post("/api/plugins/reload") +async def reload_plugins() -> list[dict[str, Any]]: + runtime.load_plugins() + plugins = runtime.plugins.public_plugins() + await runtime.ws.broadcast("plugins.loaded", {"plugins": plugins}) + await runtime.ws.broadcast("state.updated", runtime.public_state()) + return plugins + + +@app.post("/api/profiles") +async def create_profile(payload: ProfileCreate) -> dict[str, Any]: + runtime.db.create_profile(payload.name) + state = runtime.public_state() + await runtime.ws.broadcast("state.updated", state) + return state + + +@app.put("/api/profiles/{profile_id}") +async def update_profile(profile_id: str, payload: ProfileUpdate) -> dict[str, Any]: + runtime.db.update_profile(profile_id, payload.name, payload.active) + state = runtime.public_state() + await runtime.ws.broadcast("state.updated", state) + return state + + +@app.delete("/api/profiles/{profile_id}") +async def delete_profile(profile_id: str) -> dict[str, Any]: + try: + runtime.db.delete_profile(profile_id) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + state = runtime.public_state() + await runtime.ws.broadcast("state.updated", state) + return state + + +@app.post("/api/folders") +async def create_folder(payload: FolderCreate) -> dict[str, Any]: + runtime.db.create_folder(payload.profile_id, payload.parent_id, payload.name) + state = runtime.public_state() + await runtime.ws.broadcast("state.updated", state) + return state + + +@app.put("/api/folders/{folder_id}") +async def update_folder(folder_id: str, payload: FolderUpdate) -> dict[str, Any]: + runtime.db.update_folder(folder_id, payload.name, payload.parent_id) + state = runtime.public_state() + await runtime.ws.broadcast("state.updated", state) + return state + + +@app.delete("/api/folders/{folder_id}") +async def delete_folder(folder_id: str) -> dict[str, Any]: + try: + runtime.db.delete_folder(folder_id) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + state = runtime.public_state() + await runtime.ws.broadcast("state.updated", state) + return state + + +@app.put("/api/buttons/{button_id}") +async def update_button(button_id: str, payload: ButtonUpdate) -> dict[str, Any]: + try: + button = runtime.db.update_button(button_id, payload.model_dump(exclude_unset=True)) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + await runtime.ws.broadcast("state.updated", runtime.public_state()) + return button + + +@app.post("/api/actions/test") +async def test_action(payload: ActionTest) -> dict[str, str]: + if runtime.db.get_setting("click_check"): + raise HTTPException(status_code=409, detail="Click-check mode is enabled; actions are blocked.") + await runtime.actions.execute(payload.action_type, payload.action_config) + return {"status": "ok"} + + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket) -> None: + await runtime.ws.connect(websocket) + await websocket.send_json({"type": "state.updated", "payload": runtime.public_state()}) + try: + while True: + command = WebSocketCommand.model_validate(await websocket.receive_json()) + await handle_ws_command(command) + except WebSocketDisconnect: + await runtime.ws.disconnect(websocket) + + +async def handle_ws_command(command: WebSocketCommand) -> None: + payload = command.payload + if command.type == "set_active_profile": + runtime.db.update_profile(str(payload["profile_id"]), active=True) + elif command.type == "set_active_folder": + runtime.db.set_setting("active_folder_id", str(payload["folder_id"])) + elif command.type == "move_button": + folder_id = str(payload.get("folder_id") or runtime.db.get_setting("active_folder_id")) + try: + runtime.db.move_physical_button(folder_id, int(payload["position"]), int(payload["physical_button"])) + except ValueError as exc: + await runtime.ws.broadcast("action.failed", {"error": str(exc)}) + else: + await runtime.ws.broadcast("button.mapped", payload) + elif command.type == "toggle_click_check": + runtime.db.set_setting("click_check", bool(payload["enabled"])) + elif command.type == "test_action": + if runtime.db.get_setting("click_check"): + await runtime.ws.broadcast("action.failed", {"error": "Click-check mode is enabled; actions are blocked."}) + else: + await runtime.actions.execute(str(payload["action_type"]), payload.get("action_config", {})) + await runtime.ws.broadcast("state.updated", runtime.public_state()) + + +if FRONTEND_DIST.exists(): + assets = FRONTEND_DIST / "assets" + if assets.exists(): + app.mount("/assets", StaticFiles(directory=assets), name="assets") + + +@app.get("/{full_path:path}") +def serve_frontend(full_path: str): + index = FRONTEND_DIST / "index.html" + target = FRONTEND_DIST / full_path + if full_path and target.is_file(): + return FileResponse(target) + if index.exists(): + return FileResponse(index) + return { + "message": "Frontend has not been built yet. Run npm install and npm run build in frontend/.", + "api": "/api/state", + } diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..cad63be --- /dev/null +++ b/backend/models.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, Field + + +TriggerMode = Literal["down", "up"] +ActionType = Literal["noop", "key_combo", "chain", "app_launch", "folder", "folder_rotation", "plugin"] + + +class SettingsUpdate(BaseModel): + serial_port: str | None = None + click_check: bool | None = None + active_profile_id: str | None = None + active_folder_id: str | None = None + + +class ProfileCreate(BaseModel): + name: str = Field(min_length=1, max_length=80) + + +class ProfileUpdate(BaseModel): + name: str | None = Field(default=None, min_length=1, max_length=80) + active: bool | None = None + + +class FolderCreate(BaseModel): + profile_id: str + parent_id: str | None = None + name: str = Field(min_length=1, max_length=80) + + +class FolderUpdate(BaseModel): + name: str | None = Field(default=None, min_length=1, max_length=80) + parent_id: str | None = None + + +class ButtonUpdate(BaseModel): + label: str | None = None + color: str | None = None + icon: str | None = None + physical_button: int | None = Field(default=None, ge=1, le=10) + trigger_mode: TriggerMode | None = None + action_type: ActionType | None = None + action_config: dict[str, Any] | None = None + + +class ManualAppCreate(BaseModel): + name: str = Field(min_length=1, max_length=160) + path: str = Field(min_length=1) + args: str | None = None + + +class ActionTest(BaseModel): + action_type: ActionType + action_config: dict[str, Any] = Field(default_factory=dict) + + +class WebSocketCommand(BaseModel): + type: str + payload: dict[str, Any] = Field(default_factory=dict) diff --git a/backend/services/actions.py b/backend/services/actions.py new file mode 100644 index 0000000..5a29fe8 --- /dev/null +++ b/backend/services/actions.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import asyncio +from typing import Any + +from backend.services.apps import AppDiscovery +from backend.services.plugins import PluginContext + + +SPECIAL_KEYS = { + "ctrl": "ctrl", + "control": "ctrl", + "shift": "shift", + "alt": "alt", + "cmd": "cmd", + "win": "cmd", + "enter": "enter", + "return": "enter", + "esc": "esc", + "escape": "esc", + "tab": "tab", + "space": "space", + "backspace": "backspace", + "delete": "delete", + "up": "up", + "down": "down", + "left": "left", + "right": "right", +} + + +class KeyPresser: + def __init__(self) -> None: + self._controller = None + self._key = None + + def _ensure(self) -> None: + if self._controller is not None: + return + try: + from pynput.keyboard import Controller, Key + except ImportError as exc: + raise RuntimeError("pynput is not installed. Run: python -m pip install -r requirements.txt") from exc + self._controller = Controller() + self._key = Key + + async def press_combo(self, combo: str) -> None: + await asyncio.to_thread(self._press_combo_sync, combo) + + def _press_combo_sync(self, combo: str) -> None: + self._ensure() + parts = [part.strip().lower() for part in combo.replace(" ", "").split("+") if part.strip()] + if not parts: + raise ValueError("Key combo is empty.") + keys = [self._resolve_key(part) for part in parts] + for key in keys[:-1]: + self._controller.press(key) + self._controller.press(keys[-1]) + self._controller.release(keys[-1]) + for key in reversed(keys[:-1]): + self._controller.release(key) + + def _resolve_key(self, name: str) -> Any: + mapped = SPECIAL_KEYS.get(name, name) + if len(mapped) == 1: + return mapped + key = getattr(self._key, mapped, None) + if key is None: + raise ValueError(f"Unknown key '{name}'.") + return key + + +class ActionEngine: + def __init__(self, app: Any): + self.app = app + self.keys = KeyPresser() + self.apps = AppDiscovery(app.db) + + async def handle_button_event(self, event: dict[str, Any]) -> None: + settings = self.app.db.settings() + profile_id = settings.get("active_profile_id") + folder_id = settings.get("active_folder_id") + if not profile_id or not folder_id: + return + + button = self.app.db.find_button_for_event(profile_id, folder_id, int(event["button"])) + payload = {"event": event, "button": button} + await self.app.ws.broadcast(f"button.{event['event']}", payload) + + await self.app.plugins.on_event(PluginContext(self.app), {"type": f"button.{event['event']}", "payload": payload}) + + if settings.get("click_check"): + await self.app.ws.broadcast("click_check.event", payload) + return + + if not button or button["trigger_mode"] != event["event"]: + return + await self.execute(button["action_type"], button["action_config"], event, source_button=button) + + async def execute(self, action_type: str, config: dict[str, Any], event: dict[str, Any] | None = None, source_button: dict[str, Any] | None = None) -> None: + action_id = config.get("id") or action_type + payload = {"action_type": action_type, "action_config": config, "button": source_button, "event": event} + await self.app.ws.broadcast("action.started", payload) + try: + await self._execute(action_type, config, event) + except Exception as exc: + await self.app.ws.broadcast("action.failed", payload | {"error": str(exc)}) + raise + await self.app.ws.broadcast("action.finished", payload | {"id": action_id}) + + async def _execute(self, action_type: str, config: dict[str, Any], event: dict[str, Any] | None) -> None: + if action_type == "noop": + return + if action_type == "key_combo": + await self.keys.press_combo(str(config.get("combo", ""))) + return + if action_type == "chain": + for step in config.get("steps", []): + await self._execute(step.get("action_type", "noop"), step.get("action_config", {}), event) + delay_ms = int(step.get("delay_ms", 0) or 0) + if delay_ms > 0: + await asyncio.sleep(delay_ms / 1000) + return + if action_type == "app_launch": + await asyncio.to_thread(self.apps.launch, config) + return + if action_type == "folder": + folder_id = config.get("folder_id") + if not folder_id: + raise ValueError("Folder action requires folder_id.") + self.app.db.set_setting("active_folder_id", folder_id) + await self.app.ws.broadcast("state.updated", self.app.public_state()) + return + if action_type == "folder_rotation": + settings = self.app.db.settings() + profile_id = settings.get("active_profile_id") + folder_id = settings.get("active_folder_id") + if not profile_id or not folder_id: + raise ValueError("Folder rotation requires an active profile and folder.") + next_folder_id = self.app.db.next_folder_id(profile_id, folder_id, str(config.get("direction", "next"))) + if not next_folder_id: + raise ValueError("No folders are available for rotation.") + self.app.db.set_setting("active_folder_id", next_folder_id) + await self.app.ws.broadcast("state.updated", self.app.public_state()) + return + if action_type == "plugin": + await self.app.plugins.execute_action( + PluginContext(self.app), + str(config.get("plugin_id", "")), + str(config.get("action_id", "")), + config.get("fields", {}), + event, + ) + return + raise ValueError(f"Unknown action type '{action_type}'.") diff --git a/backend/services/apps.py b/backend/services/apps.py new file mode 100644 index 0000000..5db9846 --- /dev/null +++ b/backend/services/apps.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +import os +import subprocess +from pathlib import Path +from typing import Any + +from backend.database import Database + + +def _start_menu_dirs() -> list[Path]: + paths = [ + Path(os.environ.get("ProgramData", "")) / "Microsoft" / "Windows" / "Start Menu" / "Programs", + Path(os.environ.get("APPDATA", "")) / "Microsoft" / "Windows" / "Start Menu" / "Programs", + ] + return [path for path in paths if path.exists()] + + +class AppDiscovery: + def __init__(self, db: Database): + self.db = db + + def discover(self) -> list[dict[str, Any]]: + apps: list[dict[str, Any]] = [] + apps.extend(self._start_menu_apps()) + apps.extend(self._registry_apps()) + apps.extend(self.db.manual_apps()) + return self._dedupe(apps) + + def launch(self, config: dict[str, Any]) -> None: + path = config.get("path") + args = config.get("args") or "" + if not path: + raise ValueError("App launch action requires a path.") + if path.lower().endswith((".lnk", ".url", ".bat", ".cmd")): + os.startfile(path) # type: ignore[attr-defined] + return + if Path(path).suffix.lower() == ".exe": + subprocess.Popen([path, *self._split_args(args)], close_fds=True) + return + os.startfile(path) # type: ignore[attr-defined] + + def _start_menu_apps(self) -> list[dict[str, Any]]: + apps: list[dict[str, Any]] = [] + for root in _start_menu_dirs(): + for shortcut in root.rglob("*.lnk"): + apps.append( + { + "id": f"shortcut:{shortcut}", + "name": shortcut.stem, + "path": str(shortcut), + "args": None, + "source": "start_menu", + } + ) + return apps + + def _registry_apps(self) -> list[dict[str, Any]]: + apps: list[dict[str, Any]] = [] + try: + import winreg + except ImportError: + return apps + + roots = [ + (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), + (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"), + (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), + ] + for hive, key_path in roots: + try: + key = winreg.OpenKey(hive, key_path) + except OSError: + continue + with key: + for index in range(winreg.QueryInfoKey(key)[0]): + try: + sub_name = winreg.EnumKey(key, index) + sub_key = winreg.OpenKey(key, sub_name) + except OSError: + continue + with sub_key: + name = self._reg_value(sub_key, "DisplayName") + if not name: + continue + path = self._best_registry_path(sub_key) + if not path: + continue + apps.append( + { + "id": f"registry:{sub_name}", + "name": name, + "path": path, + "args": None, + "source": "registry", + } + ) + return apps + + def _best_registry_path(self, key: Any) -> str | None: + candidates = [ + self._reg_value(key, "InstallLocation"), + self._reg_value(key, "DisplayIcon"), + ] + for candidate in candidates: + if not candidate: + continue + cleaned = candidate.strip('"').split(",")[0] + path = Path(cleaned) + if path.is_file() and path.suffix.lower() == ".exe": + return str(path) + if path.is_dir(): + exes = sorted(path.glob("*.exe")) + if exes: + return str(exes[0]) + return None + + def _reg_value(self, key: Any, name: str) -> str | None: + try: + value, _ = __import__("winreg").QueryValueEx(key, name) + except OSError: + return None + return str(value) if value else None + + def _dedupe(self, apps: list[dict[str, Any]]) -> list[dict[str, Any]]: + seen: set[tuple[str, str]] = set() + result: list[dict[str, Any]] = [] + for app in sorted(apps, key=lambda item: item["name"].lower()): + key = (app["name"].lower(), str(app["path"]).lower()) + if key in seen: + continue + seen.add(key) + result.append(app) + return result + + def _split_args(self, args: str) -> list[str]: + if not args: + return [] + import shlex + + return shlex.split(args, posix=False) diff --git a/backend/services/pico.py b/backend/services/pico.py new file mode 100644 index 0000000..76a9126 --- /dev/null +++ b/backend/services/pico.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import json +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class PicoEvent: + button: int + pin: int + event: str + pressed: bool + + def to_dict(self) -> dict[str, Any]: + return { + "button": self.button, + "pin": self.pin, + "event": self.event, + "pressed": self.pressed, + } + + +def parse_pico_line(raw_line: str) -> PicoEvent | None: + try: + payload = json.loads(raw_line) + except json.JSONDecodeError: + return None + + if not isinstance(payload, dict): + return None + if not {"button", "pin", "event", "pressed"}.issubset(payload): + return None + if payload["event"] not in {"down", "up"}: + return None + + try: + button = int(payload["button"]) + pin = int(payload["pin"]) + except (TypeError, ValueError): + return None + + if button < 1 or button > 10: + return None + + return PicoEvent(button=button, pin=pin, event=payload["event"], pressed=bool(payload["pressed"])) + diff --git a/backend/services/plugins.py b/backend/services/plugins.py new file mode 100644 index 0000000..50637b2 --- /dev/null +++ b/backend/services/plugins.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import importlib.util +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +@dataclass +class LoadedPlugin: + id: str + name: str + desc: str + version: str + actions: list[dict[str, Any]] + instance: Any | None + enabled: bool + error: str | None = None + + def public(self) -> dict[str, Any]: + return { + "id": self.id, + "name": self.name, + "desc": self.desc, + "version": self.version, + "actions": self.actions, + "enabled": self.enabled, + "error": self.error, + } + + +class PluginContext: + def __init__(self, app: Any): + self.app = app + self.db = app.db + self.broadcast = app.ws.broadcast + + +class PluginManager: + def __init__(self, root: Path): + self.root = root + self.plugins: dict[str, LoadedPlugin] = {} + + def load_all(self, ctx: PluginContext) -> None: + self.root.mkdir(parents=True, exist_ok=True) + self.plugins = {} + for path in sorted(self.root.iterdir()): + if path.name.startswith("_"): + continue + if path.is_file() and path.suffix == ".py": + self._load_path(path.stem, path, ctx) + elif path.is_dir() and (path / "__init__.py").exists(): + self._load_path(path.name, path / "__init__.py", ctx) + + def public_plugins(self) -> list[dict[str, Any]]: + return [plugin.public() for plugin in self.plugins.values()] + + async def on_event(self, ctx: PluginContext, event: dict[str, Any]) -> None: + for plugin in self.plugins.values(): + if not plugin.enabled or plugin.instance is None: + continue + hook = getattr(plugin.instance, "on_event", None) + if not hook: + continue + try: + result = hook(ctx, event) + if hasattr(result, "__await__"): + await result + except Exception as exc: + plugin.enabled = False + plugin.error = f"on_event failed: {exc}" + + async def execute_action(self, ctx: PluginContext, plugin_id: str, action_id: str, config: dict[str, Any], event: dict[str, Any] | None) -> None: + plugin = self.plugins.get(plugin_id) + if not plugin or not plugin.enabled or plugin.instance is None: + raise ValueError(f"Plugin '{plugin_id}' is not available.") + hook = getattr(plugin.instance, "execute_action", None) + if not hook: + raise ValueError(f"Plugin '{plugin_id}' does not expose execute_action.") + result = hook(ctx, action_id, config, event) + if hasattr(result, "__await__"): + await result + + def _load_path(self, plugin_id: str, path: Path, ctx: PluginContext) -> None: + try: + module_name = f"streamdeck_plugin_{plugin_id}" + spec = importlib.util.spec_from_file_location(module_name, path) + if spec is None or spec.loader is None: + raise RuntimeError("Could not create import spec.") + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + instance = getattr(module, "PLUGIN") + plugin = LoadedPlugin( + id=plugin_id, + name=str(getattr(instance, "name")), + desc=str(getattr(instance, "desc", "")), + version=str(getattr(instance, "version", "0.1.0")), + actions=list(getattr(instance, "actions", [])), + instance=instance, + enabled=True, + ) + on_load = getattr(instance, "on_load", None) + if on_load: + on_load(ctx) + self.plugins[plugin_id] = plugin + except Exception as exc: + self.plugins[plugin_id] = LoadedPlugin( + id=plugin_id, + name=plugin_id, + desc="Plugin failed to load.", + version="unknown", + actions=[], + instance=None, + enabled=False, + error=str(exc), + ) + diff --git a/backend/services/serial_service.py b/backend/services/serial_service.py new file mode 100644 index 0000000..60b3049 --- /dev/null +++ b/backend/services/serial_service.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import asyncio +from typing import Any + +import serial +import serial.tools.list_ports + +from backend.services.pico import parse_pico_line + + +DEFAULT_BAUD = 115200 + + +def find_pico_port() -> str | None: + for port in serial.tools.list_ports.comports(): + vid = port.vid + manufacturer = (port.manufacturer or "").lower() + description = (port.description or "").lower() + if vid == 0x2E8A or "pico" in description or "raspberry" in manufacturer: + return port.device + return None + + +class SerialService: + def __init__(self, app: Any): + self.app = app + self.task: asyncio.Task[None] | None = None + self.stop_event = asyncio.Event() + self.connected_port: str | None = None + + def start(self) -> None: + if self.task is None or self.task.done(): + self.stop_event.clear() + self.task = asyncio.create_task(self._run()) + + async def stop(self) -> None: + self.stop_event.set() + if self.task: + await asyncio.wait([self.task], timeout=2) + + async def restart(self) -> None: + await self.stop() + self.task = None + self.start() + + async def _run(self) -> None: + while not self.stop_event.is_set(): + port = self.app.db.get_setting("serial_port") or find_pico_port() + if not port: + self.connected_port = None + await self.app.ws.broadcast("serial.disconnected", {"reason": "Pico not found"}) + await asyncio.sleep(2) + continue + try: + await self._read_port(port) + except Exception as exc: + self.connected_port = None + await self.app.ws.broadcast("serial.disconnected", {"port": port, "error": str(exc)}) + await asyncio.sleep(2) + + async def _read_port(self, port: str) -> None: + with serial.Serial(port, DEFAULT_BAUD, timeout=1) as ser: + self.connected_port = port + await self.app.ws.broadcast("serial.connected", {"port": port, "baud": DEFAULT_BAUD}) + while not self.stop_event.is_set(): + raw = await asyncio.to_thread(ser.readline) + line = raw.decode("utf-8", errors="replace").strip() + if not line: + continue + event = parse_pico_line(line) + if event is None: + await self.app.ws.broadcast("serial.diagnostic", {"line": line}) + continue + payload = event.to_dict() + self.app.db.add_event(f"button.{payload['event']}", payload) + await self.app.actions.handle_button_event(payload) + diff --git a/backend/services/websocket_manager.py b/backend/services/websocket_manager.py new file mode 100644 index 0000000..e8ed367 --- /dev/null +++ b/backend/services/websocket_manager.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import asyncio +from typing import Any + +from fastapi import WebSocket + + +class WebSocketManager: + def __init__(self) -> None: + self._clients: set[WebSocket] = set() + self._lock = asyncio.Lock() + + async def connect(self, websocket: WebSocket) -> None: + await websocket.accept() + async with self._lock: + self._clients.add(websocket) + + async def disconnect(self, websocket: WebSocket) -> None: + async with self._lock: + self._clients.discard(websocket) + + async def broadcast(self, event_type: str, payload: dict[str, Any] | None = None) -> None: + message = {"type": event_type, "payload": payload or {}} + async with self._lock: + clients = list(self._clients) + stale: list[WebSocket] = [] + for client in clients: + try: + await client.send_json(message) + except Exception: + stale.append(client) + if stale: + async with self._lock: + for client in stale: + self._clients.discard(client) + diff --git a/button_log.txt b/button_log.txt new file mode 100644 index 0000000..b8c7762 --- /dev/null +++ b/button_log.txt @@ -0,0 +1,2068 @@ +2026-05-09T22:59:52 button=2 pin=GP27 event=down +2026-05-09T22:59:53 button=2 pin=GP27 event=up +2026-05-09T22:59:54 button=3 pin=GP26 event=down +2026-05-09T22:59:54 button=3 pin=GP26 event=up +2026-05-09T22:59:55 button=4 pin=GP22 event=down +2026-05-09T22:59:55 button=4 pin=GP22 event=up +2026-05-09T22:59:55 button=5 pin=GP21 event=down +2026-05-09T22:59:56 button=5 pin=GP21 event=up +2026-05-09T22:59:56 button=6 pin=GP20 event=down +2026-05-09T22:59:56 button=6 pin=GP20 event=up +2026-05-09T22:59:57 button=8 pin=GP19 event=down +2026-05-09T22:59:57 button=8 pin=GP19 event=up +2026-05-09T23:00:00 button=10 pin=GP16 event=down +2026-05-09T23:00:00 button=10 pin=GP16 event=up +2026-05-09T23:00:01 button=1 pin=GP28 event=down +2026-05-09T23:00:01 button=1 pin=GP28 event=up +2026-05-09T23:00:02 button=10 pin=GP16 event=down +2026-05-09T23:00:02 button=10 pin=GP16 event=up +2026-05-09T23:00:04 button=8 pin=GP19 event=down +2026-05-09T23:00:04 button=8 pin=GP19 event=up +2026-05-09T23:00:09 button=1 pin=GP28 event=down +2026-05-09T23:00:09 button=1 pin=GP28 event=up +2026-05-09T23:00:10 button=10 pin=GP16 event=down +2026-05-09T23:00:10 button=10 pin=GP16 event=up +2026-05-09T23:00:10 button=1 pin=GP28 event=down +2026-05-09T23:00:10 button=1 pin=GP28 event=up +2026-05-09T23:00:11 button=10 pin=GP16 event=down +2026-05-09T23:00:11 button=10 pin=GP16 event=up +2026-05-09T23:00:12 button=8 pin=GP19 event=down +2026-05-09T23:00:13 button=8 pin=GP19 event=up +2026-05-09T23:00:13 button=6 pin=GP20 event=down +2026-05-09T23:00:13 button=6 pin=GP20 event=up +2026-05-09T23:00:14 button=5 pin=GP21 event=down +2026-05-09T23:00:14 button=5 pin=GP21 event=up +2026-05-09T23:00:14 button=4 pin=GP22 event=down +2026-05-09T23:00:14 button=4 pin=GP22 event=up +2026-05-09T23:00:15 button=3 pin=GP26 event=down +2026-05-09T23:00:15 button=3 pin=GP26 event=up +2026-05-09T23:00:15 button=2 pin=GP27 event=down +2026-05-09T23:00:16 button=2 pin=GP27 event=up +2026-05-09T23:00:19 button=10 pin=GP16 event=down +2026-05-09T23:00:19 button=10 pin=GP16 event=up +2026-05-09T23:00:20 button=10 pin=GP16 event=down +2026-05-09T23:00:20 button=10 pin=GP16 event=up +2026-05-09T23:00:38 button=10 pin=GP16 event=down +2026-05-09T23:00:38 button=10 pin=GP16 event=up +2026-05-09T23:00:38 button=10 pin=GP16 event=down +2026-05-09T23:00:38 button=10 pin=GP16 event=up +2026-05-09T23:01:06 button=2 pin=GP27 event=down +2026-05-09T23:01:06 button=2 pin=GP27 event=up +2026-05-09T23:01:33 button=3 pin=GP26 event=down +2026-05-09T23:01:33 button=3 pin=GP26 event=up +2026-05-09T23:01:33 button=4 pin=GP22 event=down +2026-05-09T23:01:33 button=4 pin=GP22 event=up +2026-05-09T23:01:34 button=3 pin=GP26 event=down +2026-05-09T23:01:34 button=3 pin=GP26 event=up +2026-05-09T23:01:35 button=3 pin=GP26 event=down +2026-05-09T23:01:35 button=3 pin=GP26 event=up +2026-05-09T23:01:35 button=4 pin=GP22 event=down +2026-05-09T23:01:36 button=4 pin=GP22 event=up +2026-05-09T23:01:36 button=5 pin=GP21 event=down +2026-05-09T23:01:36 button=5 pin=GP21 event=up +2026-05-09T23:01:36 button=6 pin=GP20 event=down +2026-05-09T23:01:37 button=6 pin=GP20 event=up +2026-05-09T23:01:37 button=8 pin=GP19 event=down +2026-05-09T23:01:37 button=8 pin=GP19 event=up +2026-05-09T23:01:38 button=7 pin=GP18 event=down +2026-05-09T23:01:39 button=7 pin=GP18 event=up +2026-05-09T23:01:40 button=10 pin=GP16 event=down +2026-05-09T23:01:40 button=10 pin=GP16 event=up +2026-05-09T23:01:50 button=1 pin=GP28 event=down +2026-05-09T23:01:51 button=1 pin=GP28 event=up +2026-05-09T23:01:52 button=1 pin=GP28 event=down +2026-05-09T23:01:52 button=1 pin=GP28 event=up +2026-05-09T23:01:52 button=1 pin=GP28 event=down +2026-05-09T23:01:52 button=1 pin=GP28 event=up +2026-05-09T23:01:52 button=1 pin=GP28 event=down +2026-05-09T23:01:52 button=1 pin=GP28 event=up +2026-05-09T23:01:52 button=1 pin=GP28 event=down +2026-05-09T23:01:53 button=1 pin=GP28 event=up +2026-05-09T23:04:39 button=1 pin=GP28 event=down +2026-05-09T23:04:39 button=1 pin=GP28 event=up +2026-05-09T23:04:39 button=1 pin=GP28 event=down +2026-05-09T23:04:39 button=1 pin=GP28 event=up +2026-05-09T23:04:40 button=10 pin=GP16 event=down +2026-05-09T23:04:40 button=10 pin=GP16 event=up +2026-05-09T23:04:41 button=10 pin=GP16 event=down +2026-05-09T23:04:41 button=10 pin=GP16 event=up +2026-05-09T23:05:05 button=9 pin=GP17 event=down +2026-05-09T23:05:05 button=9 pin=GP17 event=up +2026-05-09T23:05:08 button=1 pin=GP28 event=down +2026-05-09T23:05:09 button=1 pin=GP28 event=up +2026-05-09T23:06:03 button=10 pin=GP16 event=down +2026-05-09T23:06:03 button=10 pin=GP16 event=up +2026-05-09T23:06:04 button=10 pin=GP16 event=down +2026-05-09T23:06:05 button=10 pin=GP16 event=up +2026-05-09T23:06:06 button=1 pin=GP28 event=down +2026-05-09T23:06:06 button=1 pin=GP28 event=up +2026-05-09T23:06:16 button=1 pin=GP28 event=down +2026-05-09T23:06:16 button=1 pin=GP28 event=up +2026-05-09T23:06:16 button=1 pin=GP28 event=down +2026-05-09T23:06:16 button=1 pin=GP28 event=up +2026-05-09T23:06:17 button=10 pin=GP16 event=down +2026-05-09T23:06:17 button=10 pin=GP16 event=up +2026-05-09T23:06:17 button=10 pin=GP16 event=down +2026-05-09T23:06:17 button=10 pin=GP16 event=up +2026-05-09T23:06:32 button=7 pin=GP18 event=down +2026-05-09T23:06:32 button=7 pin=GP18 event=up +2026-05-09T23:06:32 button=7 pin=GP18 event=down +2026-05-09T23:06:32 button=7 pin=GP18 event=up +2026-05-09T23:06:36 button=9 pin=GP17 event=down +2026-05-09T23:06:36 button=9 pin=GP17 event=up +2026-05-09T23:06:36 button=9 pin=GP17 event=down +2026-05-09T23:06:37 button=9 pin=GP17 event=up +2026-05-09T23:06:37 button=9 pin=GP17 event=down +2026-05-09T23:06:38 button=9 pin=GP17 event=up +2026-05-09T23:06:40 button=9 pin=GP17 event=down +2026-05-09T23:06:40 button=9 pin=GP17 event=up +2026-05-09T23:06:41 button=9 pin=GP17 event=down +2026-05-09T23:06:41 button=9 pin=GP17 event=up +2026-05-09T23:06:45 button=9 pin=GP17 event=down +2026-05-09T23:06:45 button=9 pin=GP17 event=up +2026-05-09T23:06:45 button=9 pin=GP17 event=down +2026-05-09T23:06:46 button=9 pin=GP17 event=up +2026-05-09T23:07:31 button=10 pin=GP16 event=down +2026-05-09T23:07:31 button=1 pin=GP28 event=down +2026-05-09T23:07:34 button=1 pin=GP28 event=up +2026-05-09T23:07:34 button=1 pin=GP28 event=down +2026-05-09T23:07:34 button=1 pin=GP28 event=up +2026-05-09T23:07:43 button=10 pin=GP16 event=up +2026-05-09T23:07:44 button=10 pin=GP16 event=down +2026-05-09T23:07:47 button=10 pin=GP16 event=up +2026-05-09T23:07:52 button=9 pin=GP17 event=down +2026-05-09T23:07:53 button=9 pin=GP17 event=up +2026-05-09T23:07:54 button=9 pin=GP17 event=down +2026-05-09T23:07:55 button=9 pin=GP17 event=up +2026-05-09T23:07:55 button=9 pin=GP17 event=down +2026-05-09T23:07:55 button=9 pin=GP17 event=up +2026-05-09T23:07:55 button=9 pin=GP17 event=down +2026-05-09T23:07:55 button=9 pin=GP17 event=up +2026-05-09T23:07:56 button=7 pin=GP18 event=down +2026-05-09T23:07:56 button=7 pin=GP18 event=up +2026-05-09T23:07:57 button=8 pin=GP19 event=down +2026-05-09T23:07:57 button=8 pin=GP19 event=up +2026-05-09T23:07:58 button=6 pin=GP20 event=down +2026-05-09T23:07:58 button=6 pin=GP20 event=up +2026-05-09T23:07:58 button=5 pin=GP21 event=down +2026-05-09T23:07:58 button=5 pin=GP21 event=up +2026-05-09T23:07:59 button=4 pin=GP22 event=down +2026-05-09T23:07:59 button=4 pin=GP22 event=up +2026-05-09T23:08:00 button=4 pin=GP22 event=down +2026-05-09T23:08:00 button=4 pin=GP22 event=up +2026-05-09T23:08:00 button=3 pin=GP26 event=down +2026-05-09T23:08:01 button=3 pin=GP26 event=up +2026-05-09T23:08:05 button=2 pin=GP27 event=down +2026-05-09T23:08:05 button=3 pin=GP26 event=down +2026-05-09T23:08:05 button=2 pin=GP27 event=up +2026-05-09T23:08:05 button=2 pin=GP27 event=down +2026-05-09T23:08:05 button=2 pin=GP27 event=up +2026-05-09T23:08:05 button=2 pin=GP27 event=down +2026-05-09T23:08:06 button=3 pin=GP26 event=up +2026-05-09T23:08:06 button=3 pin=GP26 event=down +2026-05-09T23:08:06 button=3 pin=GP26 event=up +2026-05-09T23:08:06 button=2 pin=GP27 event=up +2026-05-09T23:08:06 button=2 pin=GP27 event=down +2026-05-09T23:08:06 button=2 pin=GP27 event=up +2026-05-09T23:08:06 button=2 pin=GP27 event=down +2026-05-09T23:08:06 button=2 pin=GP27 event=up +2026-05-09T23:08:06 button=2 pin=GP27 event=down +2026-05-09T23:08:07 button=2 pin=GP27 event=up +2026-05-09T23:08:07 button=2 pin=GP27 event=down +2026-05-09T23:08:07 button=2 pin=GP27 event=up +2026-05-09T23:08:07 button=2 pin=GP27 event=down +2026-05-09T23:08:07 button=2 pin=GP27 event=up +2026-05-09T23:08:07 button=2 pin=GP27 event=down +2026-05-09T23:08:07 button=2 pin=GP27 event=up +2026-05-09T23:08:07 button=2 pin=GP27 event=down +2026-05-09T23:08:08 button=2 pin=GP27 event=up +2026-05-09T23:08:08 button=2 pin=GP27 event=down +2026-05-09T23:08:08 button=2 pin=GP27 event=up +2026-05-09T23:08:08 button=2 pin=GP27 event=down +2026-05-09T23:08:08 button=2 pin=GP27 event=up +2026-05-09T23:08:08 button=2 pin=GP27 event=down +2026-05-09T23:08:08 button=2 pin=GP27 event=up +2026-05-09T23:08:08 button=2 pin=GP27 event=down +2026-05-09T23:08:08 button=2 pin=GP27 event=up +2026-05-09T23:08:08 button=2 pin=GP27 event=down +2026-05-09T23:08:08 button=2 pin=GP27 event=up +2026-05-09T23:08:09 button=2 pin=GP27 event=down +2026-05-09T23:08:09 button=2 pin=GP27 event=up +2026-05-09T23:08:20 button=2 pin=GP27 event=down +2026-05-09T23:08:20 button=2 pin=GP27 event=up +2026-05-09T23:08:20 button=2 pin=GP27 event=down +2026-05-09T23:08:20 button=2 pin=GP27 event=up +2026-05-09T23:08:21 button=2 pin=GP27 event=down +2026-05-09T23:08:21 button=2 pin=GP27 event=up +2026-05-09T23:08:21 button=2 pin=GP27 event=down +2026-05-09T23:08:21 button=2 pin=GP27 event=up +2026-05-09T23:08:36 button=1 pin=GP28 event=down +2026-05-09T23:08:36 button=1 pin=GP28 event=up +2026-05-09T23:08:36 button=10 pin=GP16 event=down +2026-05-09T23:08:36 button=10 pin=GP16 event=up +2026-05-09T23:08:36 button=9 pin=GP17 event=down +2026-05-09T23:08:37 button=9 pin=GP17 event=up +2026-05-09T23:08:40 button=8 pin=GP19 event=down +2026-05-09T23:08:40 button=8 pin=GP19 event=up +2026-05-09T23:08:41 button=6 pin=GP20 event=down +2026-05-09T23:08:41 button=6 pin=GP20 event=up +2026-05-09T23:08:41 button=5 pin=GP21 event=down +2026-05-09T23:08:42 button=5 pin=GP21 event=up +2026-05-09T23:08:48 button=8 pin=GP19 event=down +2026-05-09T23:08:48 button=8 pin=GP19 event=up +2026-05-09T23:08:48 button=6 pin=GP20 event=down +2026-05-09T23:08:48 button=6 pin=GP20 event=up +2026-05-09T23:08:49 button=5 pin=GP21 event=down +2026-05-09T23:08:49 button=5 pin=GP21 event=up +2026-05-09T23:08:50 button=3 pin=GP26 event=down +2026-05-09T23:08:51 button=3 pin=GP26 event=up +2026-05-09T23:08:51 button=2 pin=GP27 event=down +2026-05-09T23:08:52 button=2 pin=GP27 event=up +2026-05-09T23:12:41 button=5 pin=GP21 event=down +2026-05-09T23:12:41 button=5 pin=GP21 event=up +2026-05-09T23:12:43 button=3 pin=GP26 event=down +2026-05-09T23:12:43 button=3 pin=GP26 event=up +2026-05-09T23:12:43 button=3 pin=GP26 event=down +2026-05-09T23:12:43 button=3 pin=GP26 event=up +2026-05-09T23:12:46 button=2 pin=GP27 event=down +2026-05-09T23:12:46 button=2 pin=GP27 event=up +2026-05-09T23:12:46 button=3 pin=GP26 event=down +2026-05-09T23:12:46 button=3 pin=GP26 event=up +2026-05-09T23:13:04 button=6 pin=GP20 event=down +2026-05-09T23:13:04 button=4 pin=GP22 event=down +2026-05-09T23:13:05 button=4 pin=GP22 event=up +2026-05-09T23:13:05 button=4 pin=GP22 event=down +2026-05-09T23:13:06 button=4 pin=GP22 event=up +2026-05-09T23:13:06 button=4 pin=GP22 event=down +2026-05-09T23:13:07 button=4 pin=GP22 event=up +2026-05-09T23:13:07 button=6 pin=GP20 event=up +2026-05-09T23:13:07 button=4 pin=GP22 event=down +2026-05-09T23:13:07 button=4 pin=GP22 event=up +2026-05-09T23:13:07 button=4 pin=GP22 event=down +2026-05-09T23:13:08 button=4 pin=GP22 event=up +2026-05-09T23:13:08 button=4 pin=GP22 event=down +2026-05-09T23:13:08 button=4 pin=GP22 event=up +2026-05-09T23:13:08 button=4 pin=GP22 event=down +2026-05-09T23:13:08 button=4 pin=GP22 event=up +2026-05-09T23:13:08 button=4 pin=GP22 event=down +2026-05-09T23:13:09 button=4 pin=GP22 event=up +2026-05-09T23:13:09 button=4 pin=GP22 event=down +2026-05-09T23:13:09 button=4 pin=GP22 event=up +2026-05-09T23:13:09 button=4 pin=GP22 event=down +2026-05-09T23:13:09 button=4 pin=GP22 event=up +2026-05-09T23:13:09 button=4 pin=GP22 event=down +2026-05-09T23:13:09 button=4 pin=GP22 event=up +2026-05-09T23:13:09 button=4 pin=GP22 event=down +2026-05-09T23:13:09 button=4 pin=GP22 event=up +2026-05-09T23:13:09 button=4 pin=GP22 event=down +2026-05-09T23:13:09 button=4 pin=GP22 event=up +2026-05-09T23:13:10 button=4 pin=GP22 event=down +2026-05-09T23:13:10 button=4 pin=GP22 event=up +2026-05-09T23:13:10 button=4 pin=GP22 event=down +2026-05-09T23:13:10 button=4 pin=GP22 event=up +2026-05-09T23:13:10 button=4 pin=GP22 event=down +2026-05-09T23:13:10 button=4 pin=GP22 event=up +2026-05-09T23:13:11 button=4 pin=GP22 event=down +2026-05-09T23:13:11 button=4 pin=GP22 event=up +2026-05-09T23:13:11 button=5 pin=GP21 event=down +2026-05-09T23:13:12 button=5 pin=GP21 event=up +2026-05-09T23:13:12 button=4 pin=GP22 event=down +2026-05-09T23:13:12 button=4 pin=GP22 event=up +2026-05-09T23:13:12 button=4 pin=GP22 event=down +2026-05-09T23:13:12 button=4 pin=GP22 event=up +2026-05-09T23:13:12 button=4 pin=GP22 event=down +2026-05-09T23:13:12 button=4 pin=GP22 event=up +2026-05-09T23:13:12 button=5 pin=GP21 event=down +2026-05-09T23:13:13 button=5 pin=GP21 event=up +2026-05-09T23:13:13 button=4 pin=GP22 event=down +2026-05-09T23:13:13 button=4 pin=GP22 event=up +2026-05-09T23:13:13 button=3 pin=GP26 event=down +2026-05-09T23:13:14 button=3 pin=GP26 event=up +2026-05-09T23:13:14 button=2 pin=GP27 event=down +2026-05-09T23:13:14 button=2 pin=GP27 event=up +2026-05-09T23:13:14 button=5 pin=GP21 event=down +2026-05-09T23:13:14 button=5 pin=GP21 event=up +2026-05-09T23:13:14 button=2 pin=GP27 event=down +2026-05-09T23:13:14 button=2 pin=GP27 event=up +2026-05-09T23:13:15 button=5 pin=GP21 event=down +2026-05-09T23:13:15 button=5 pin=GP21 event=up +2026-05-09T23:13:15 button=5 pin=GP21 event=down +2026-05-09T23:13:15 button=5 pin=GP21 event=up +2026-05-09T23:13:15 button=5 pin=GP21 event=down +2026-05-09T23:13:15 button=5 pin=GP21 event=up +2026-05-09T23:13:15 button=6 pin=GP20 event=down +2026-05-09T23:13:15 button=6 pin=GP20 event=up +2026-05-09T23:13:16 button=6 pin=GP20 event=down +2026-05-09T23:13:16 button=6 pin=GP20 event=up +2026-05-09T23:13:16 button=6 pin=GP20 event=down +2026-05-09T23:13:16 button=6 pin=GP20 event=up +2026-05-09T23:13:16 button=8 pin=GP19 event=down +2026-05-09T23:13:16 button=8 pin=GP19 event=up +2026-05-09T23:13:16 button=8 pin=GP19 event=down +2026-05-09T23:13:16 button=8 pin=GP19 event=up +2026-05-09T23:13:17 button=8 pin=GP19 event=down +2026-05-09T23:13:17 button=8 pin=GP19 event=up +2026-05-09T23:13:17 button=7 pin=GP18 event=down +2026-05-09T23:13:17 button=7 pin=GP18 event=up +2026-05-09T23:13:17 button=7 pin=GP18 event=down +2026-05-09T23:13:17 button=7 pin=GP18 event=up +2026-05-09T23:13:17 button=7 pin=GP18 event=down +2026-05-09T23:13:17 button=7 pin=GP18 event=up +2026-05-09T23:13:29 button=9 pin=GP17 event=down +2026-05-09T23:13:29 button=9 pin=GP17 event=up +2026-05-09T23:13:29 button=9 pin=GP17 event=down +2026-05-09T23:13:29 button=9 pin=GP17 event=up +2026-05-09T23:15:30 button=9 pin=GP17 event=down +2026-05-09T23:15:50 button=9 pin=GP17 event=up +2026-05-09T23:19:54 button=10 pin=GP16 event=down +2026-05-09T23:19:54 button=10 pin=GP16 event=up +2026-05-09T23:19:54 button=10 pin=GP16 event=down +2026-05-09T23:19:54 button=10 pin=GP16 event=up +2026-05-09T23:19:55 button=1 pin=GP28 event=down +2026-05-09T23:19:55 button=1 pin=GP28 event=up +2026-05-09T23:19:55 button=1 pin=GP28 event=down +2026-05-09T23:19:55 button=1 pin=GP28 event=up +2026-05-09T23:19:56 button=7 pin=GP18 event=down +2026-05-09T23:19:56 button=7 pin=GP18 event=up +2026-05-09T23:19:57 button=8 pin=GP19 event=down +2026-05-09T23:19:57 button=8 pin=GP19 event=up +2026-05-09T23:19:57 button=6 pin=GP20 event=down +2026-05-09T23:19:57 button=6 pin=GP20 event=up +2026-05-09T23:19:58 button=5 pin=GP21 event=down +2026-05-09T23:19:58 button=5 pin=GP21 event=up +2026-05-09T23:19:58 button=4 pin=GP22 event=down +2026-05-09T23:19:58 button=4 pin=GP22 event=up +2026-05-09T23:19:59 button=3 pin=GP26 event=down +2026-05-09T23:19:59 button=3 pin=GP26 event=up +2026-05-09T23:20:03 button=2 pin=GP27 event=down +2026-05-09T23:20:03 button=3 pin=GP26 event=down +2026-05-09T23:20:03 button=3 pin=GP26 event=up +2026-05-09T23:20:03 button=2 pin=GP27 event=up +2026-05-09T23:20:11 button=2 pin=GP27 event=down +2026-05-09T23:20:11 button=3 pin=GP26 event=down +2026-05-09T23:20:12 button=3 pin=GP26 event=up +2026-05-09T23:20:12 button=2 pin=GP27 event=up +2026-05-09T23:20:13 button=2 pin=GP27 event=down +2026-05-09T23:20:13 button=2 pin=GP27 event=up +2026-05-09T23:20:13 button=2 pin=GP27 event=down +2026-05-09T23:20:13 button=2 pin=GP27 event=up +2026-05-09T23:20:13 button=2 pin=GP27 event=down +2026-05-09T23:20:13 button=2 pin=GP27 event=up +2026-05-09T23:20:14 button=2 pin=GP27 event=down +2026-05-09T23:20:14 button=2 pin=GP27 event=up +2026-05-09T23:20:14 button=2 pin=GP27 event=down +2026-05-09T23:20:14 button=2 pin=GP27 event=up +2026-05-09T23:20:15 button=2 pin=GP27 event=down +2026-05-09T23:20:15 button=2 pin=GP27 event=up +2026-05-09T23:20:16 button=10 pin=GP16 event=down +2026-05-09T23:20:17 button=10 pin=GP16 event=up +2026-05-09T23:20:17 button=1 pin=GP28 event=down +2026-05-09T23:20:17 button=1 pin=GP28 event=up +2026-05-09T23:22:24 button=9 pin=GP17 event=down +2026-05-09T23:22:25 button=9 pin=GP17 event=up +2026-05-09T23:22:25 button=9 pin=GP17 event=down +2026-05-09T23:22:25 button=9 pin=GP17 event=up +2026-05-09T23:22:25 button=9 pin=GP17 event=down +2026-05-09T23:22:25 button=9 pin=GP17 event=up +2026-05-09T23:23:42 button=9 pin=GP17 event=down +2026-05-09T23:23:43 button=9 pin=GP17 event=up +2026-05-09T23:23:43 button=9 pin=GP17 event=down +2026-05-09T23:23:43 button=9 pin=GP17 event=up +2026-05-09T23:23:43 button=9 pin=GP17 event=down +2026-05-09T23:23:43 button=9 pin=GP17 event=up +2026-05-09T23:25:58 button=9 pin=GP17 event=down +2026-05-09T23:25:58 button=9 pin=GP17 event=up +2026-05-09T23:25:59 button=9 pin=GP17 event=down +2026-05-09T23:25:59 button=9 pin=GP17 event=up +2026-05-09T23:25:59 button=9 pin=GP17 event=down +2026-05-09T23:25:59 button=9 pin=GP17 event=up +2026-05-09T23:25:59 button=9 pin=GP17 event=down +2026-05-09T23:25:59 button=9 pin=GP17 event=up +2026-05-09T23:27:21 button=9 pin=GP17 event=down +2026-05-09T23:27:22 button=9 pin=GP17 event=up +2026-05-09T23:27:22 button=9 pin=GP17 event=down +2026-05-09T23:27:22 button=10 pin=GP16 event=down +2026-05-09T23:27:22 button=10 pin=GP16 event=up +2026-05-09T23:27:22 button=9 pin=GP17 event=up +2026-05-09T23:27:22 button=9 pin=GP17 event=down +2026-05-09T23:27:22 button=9 pin=GP17 event=up +2026-05-09T23:27:23 button=9 pin=GP17 event=down +2026-05-09T23:27:23 button=9 pin=GP17 event=up +2026-05-09T23:27:23 button=9 pin=GP17 event=down +2026-05-09T23:27:23 button=9 pin=GP17 event=up +2026-05-09T23:27:23 button=9 pin=GP17 event=down +2026-05-09T23:27:23 button=9 pin=GP17 event=up +2026-05-09T23:27:23 button=9 pin=GP17 event=down +2026-05-09T23:27:23 button=9 pin=GP17 event=up +2026-05-09T23:27:23 button=9 pin=GP17 event=down +2026-05-09T23:27:24 button=9 pin=GP17 event=up +2026-05-09T23:27:24 button=9 pin=GP17 event=down +2026-05-09T23:27:24 button=9 pin=GP17 event=up +2026-05-09T23:27:24 button=9 pin=GP17 event=down +2026-05-09T23:27:24 button=9 pin=GP17 event=up +2026-05-09T23:31:00 button=9 pin=GP17 event=down +2026-05-09T23:31:00 button=9 pin=GP17 event=up +2026-05-09T23:31:00 button=9 pin=GP17 event=down +2026-05-09T23:31:00 button=9 pin=GP17 event=up +2026-05-09T23:31:00 button=9 pin=GP17 event=down +2026-05-09T23:31:00 button=9 pin=GP17 event=up +2026-05-09T23:31:01 button=9 pin=GP17 event=down +2026-05-09T23:31:01 button=9 pin=GP17 event=up +2026-05-09T23:31:01 button=9 pin=GP17 event=down +2026-05-09T23:31:01 button=9 pin=GP17 event=up +2026-05-09T23:31:09 button=9 pin=GP17 event=down +2026-05-09T23:31:09 button=10 pin=GP16 event=down +2026-05-09T23:31:11 button=10 pin=GP16 event=up +2026-05-09T23:31:11 button=9 pin=GP17 event=up +2026-05-09T23:31:11 button=9 pin=GP17 event=down +2026-05-09T23:31:12 button=9 pin=GP17 event=up +2026-05-09T23:31:12 button=9 pin=GP17 event=down +2026-05-09T23:31:12 button=9 pin=GP17 event=up +2026-05-09T23:31:12 button=9 pin=GP17 event=down +2026-05-09T23:31:12 button=9 pin=GP17 event=up +2026-05-09T23:31:12 button=9 pin=GP17 event=down +2026-05-09T23:31:12 button=9 pin=GP17 event=up +2026-05-09T23:31:12 button=7 pin=GP18 event=down +2026-05-09T23:31:13 button=7 pin=GP18 event=up +2026-05-09T23:31:13 button=9 pin=GP17 event=down +2026-05-09T23:31:13 button=9 pin=GP17 event=up +2026-05-09T23:31:13 button=10 pin=GP16 event=down +2026-05-09T23:31:14 button=10 pin=GP16 event=up +2026-05-09T23:31:14 button=1 pin=GP28 event=down +2026-05-09T23:31:14 button=1 pin=GP28 event=up +2026-05-09T23:31:15 button=7 pin=GP18 event=down +2026-05-09T23:31:15 button=7 pin=GP18 event=up +2026-05-09T23:31:15 button=8 pin=GP19 event=down +2026-05-09T23:31:16 button=8 pin=GP19 event=up +2026-05-09T23:31:16 button=6 pin=GP20 event=down +2026-05-09T23:31:16 button=6 pin=GP20 event=up +2026-05-09T23:31:17 button=6 pin=GP20 event=down +2026-05-09T23:31:17 button=6 pin=GP20 event=up +2026-05-09T23:31:17 button=5 pin=GP21 event=down +2026-05-09T23:31:17 button=5 pin=GP21 event=up +2026-05-09T23:31:18 button=4 pin=GP22 event=down +2026-05-09T23:31:18 button=4 pin=GP22 event=up +2026-05-09T23:31:19 button=3 pin=GP26 event=down +2026-05-09T23:31:19 button=3 pin=GP26 event=up +2026-05-09T23:31:19 button=2 pin=GP27 event=down +2026-05-09T23:31:19 button=2 pin=GP27 event=up +2026-05-09T23:31:45 button=5 pin=GP21 event=down +2026-05-09T23:31:45 button=6 pin=GP20 event=down +2026-05-09T23:31:45 button=6 pin=GP20 event=up +2026-05-09T23:31:45 button=5 pin=GP21 event=up +2026-05-09T23:31:46 button=6 pin=GP20 event=down +2026-05-09T23:31:46 button=9 pin=GP17 event=down +2026-05-09T23:31:46 button=6 pin=GP20 event=up +2026-05-09T23:31:46 button=9 pin=GP17 event=up +2026-05-09T23:31:46 button=1 pin=GP28 event=down +2026-05-09T23:31:46 button=1 pin=GP28 event=up +2026-05-09T23:31:47 button=10 pin=GP16 event=down +2026-05-09T23:31:47 button=10 pin=GP16 event=up +2026-05-09T23:31:47 button=9 pin=GP17 event=down +2026-05-09T23:31:47 button=9 pin=GP17 event=up +2026-05-09T23:31:54 button=7 pin=GP18 event=down +2026-05-09T23:31:54 button=7 pin=GP18 event=up +2026-05-09T23:31:54 button=7 pin=GP18 event=down +2026-05-09T23:31:54 button=7 pin=GP18 event=up +2026-05-09T23:31:54 button=7 pin=GP18 event=down +2026-05-09T23:31:54 button=7 pin=GP18 event=up +2026-05-09T23:31:54 button=7 pin=GP18 event=down +2026-05-09T23:31:54 button=7 pin=GP18 event=up +2026-05-09T23:31:55 button=7 pin=GP18 event=down +2026-05-09T23:31:55 button=7 pin=GP18 event=up +2026-05-09T23:31:55 button=7 pin=GP18 event=down +2026-05-09T23:31:55 button=7 pin=GP18 event=up +2026-05-09T23:31:56 button=7 pin=GP18 event=down +2026-05-09T23:31:56 button=9 pin=GP17 event=down +2026-05-09T23:31:58 button=9 pin=GP17 event=up +2026-05-09T23:31:58 button=7 pin=GP18 event=up +2026-05-09T23:31:58 button=7 pin=GP18 event=down +2026-05-09T23:31:58 button=9 pin=GP17 event=down +2026-05-09T23:31:59 button=7 pin=GP18 event=up +2026-05-09T23:31:59 button=9 pin=GP17 event=up +2026-05-09T23:32:00 button=7 pin=GP18 event=down +2026-05-09T23:32:00 button=7 pin=GP18 event=up +2026-05-09T23:32:00 button=7 pin=GP18 event=down +2026-05-09T23:32:00 button=7 pin=GP18 event=up +2026-05-09T23:32:00 button=7 pin=GP18 event=down +2026-05-09T23:32:00 button=7 pin=GP18 event=up +2026-05-09T23:32:01 button=7 pin=GP18 event=down +2026-05-09T23:32:01 button=7 pin=GP18 event=up +2026-05-09T23:32:01 button=8 pin=GP19 event=down +2026-05-09T23:32:02 button=8 pin=GP19 event=up +2026-05-09T23:32:02 button=6 pin=GP20 event=down +2026-05-09T23:32:02 button=6 pin=GP20 event=up +2026-05-09T23:32:02 button=5 pin=GP21 event=down +2026-05-09T23:32:03 button=5 pin=GP21 event=up +2026-05-09T23:32:06 button=4 pin=GP22 event=down +2026-05-09T23:32:06 button=4 pin=GP22 event=up +2026-05-09T23:32:06 button=4 pin=GP22 event=down +2026-05-09T23:32:09 button=4 pin=GP22 event=up +2026-05-09T23:32:09 button=4 pin=GP22 event=down +2026-05-09T23:32:09 button=4 pin=GP22 event=up +2026-05-09T23:32:10 button=4 pin=GP22 event=down +2026-05-09T23:32:10 button=4 pin=GP22 event=up +2026-05-09T23:32:10 button=4 pin=GP22 event=down +2026-05-09T23:32:10 button=4 pin=GP22 event=up +2026-05-09T23:32:10 button=4 pin=GP22 event=down +2026-05-09T23:32:10 button=4 pin=GP22 event=up +2026-05-09T23:32:10 button=4 pin=GP22 event=down +2026-05-09T23:32:11 button=4 pin=GP22 event=up +2026-05-09T23:32:11 button=4 pin=GP22 event=down +2026-05-09T23:32:11 button=4 pin=GP22 event=up +2026-05-09T23:32:11 button=3 pin=GP26 event=down +2026-05-09T23:32:11 button=3 pin=GP26 event=up +2026-05-09T23:32:11 button=3 pin=GP26 event=down +2026-05-09T23:32:11 button=3 pin=GP26 event=up +2026-05-09T23:32:11 button=3 pin=GP26 event=down +2026-05-09T23:32:11 button=3 pin=GP26 event=up +2026-05-09T23:32:12 button=2 pin=GP27 event=down +2026-05-09T23:32:12 button=2 pin=GP27 event=up +2026-05-09T23:32:12 button=2 pin=GP27 event=down +2026-05-09T23:32:12 button=2 pin=GP27 event=up +2026-05-09T23:32:15 button=1 pin=GP28 event=down +2026-05-09T23:32:15 button=1 pin=GP28 event=up +2026-05-09T23:32:19 button=3 pin=GP26 event=down +2026-05-09T23:32:19 button=10 pin=GP16 event=down +2026-05-09T23:32:19 button=10 pin=GP16 event=up +2026-05-09T23:32:20 button=2 pin=GP27 event=down +2026-05-09T23:32:20 button=3 pin=GP26 event=up +2026-05-09T23:32:20 button=8 pin=GP19 event=down +2026-05-09T23:32:20 button=1 pin=GP28 event=down +2026-05-09T23:32:20 button=3 pin=GP26 event=down +2026-05-09T23:32:20 button=5 pin=GP21 event=down +2026-05-09T23:32:20 button=4 pin=GP22 event=down +2026-05-09T23:32:20 button=10 pin=GP16 event=down +2026-05-09T23:32:20 button=9 pin=GP17 event=down +2026-05-09T23:32:20 button=4 pin=GP22 event=up +2026-05-09T23:32:20 button=3 pin=GP26 event=up +2026-05-09T23:32:20 button=9 pin=GP17 event=up +2026-05-09T23:32:20 button=10 pin=GP16 event=up +2026-05-09T23:32:20 button=1 pin=GP28 event=up +2026-05-09T23:32:20 button=2 pin=GP27 event=up +2026-05-09T23:32:20 button=5 pin=GP21 event=up +2026-05-09T23:32:20 button=8 pin=GP19 event=up +2026-05-09T23:32:22 button=1 pin=GP28 event=down +2026-05-09T23:32:22 button=1 pin=GP28 event=up +2026-05-09T23:32:22 button=10 pin=GP16 event=down +2026-05-09T23:32:22 button=10 pin=GP16 event=up +2026-05-09T23:32:23 button=1 pin=GP28 event=down +2026-05-09T23:32:23 button=1 pin=GP28 event=up +2026-05-09T23:32:23 button=10 pin=GP16 event=down +2026-05-09T23:32:23 button=10 pin=GP16 event=up +2026-05-09T23:32:24 button=9 pin=GP17 event=down +2026-05-09T23:32:24 button=9 pin=GP17 event=up +2026-05-09T23:32:25 button=8 pin=GP19 event=down +2026-05-09T23:32:26 button=8 pin=GP19 event=up +2026-05-09T23:32:26 button=6 pin=GP20 event=down +2026-05-09T23:32:26 button=6 pin=GP20 event=up +2026-05-09T23:32:27 button=5 pin=GP21 event=down +2026-05-09T23:32:27 button=5 pin=GP21 event=up +2026-05-09T23:32:27 button=4 pin=GP22 event=down +2026-05-09T23:32:27 button=4 pin=GP22 event=up +2026-05-09T23:32:28 button=3 pin=GP26 event=down +2026-05-09T23:32:28 button=3 pin=GP26 event=up +2026-05-09T23:32:28 button=2 pin=GP27 event=down +2026-05-09T23:32:28 button=2 pin=GP27 event=up +2026-05-09T23:32:29 button=6 pin=GP20 event=down +2026-05-09T23:32:29 button=6 pin=GP20 event=up +2026-05-09T23:32:29 button=8 pin=GP19 event=down +2026-05-09T23:32:29 button=8 pin=GP19 event=up +2026-05-09T23:32:48 button=7 pin=GP18 event=down +2026-05-09T23:32:48 button=7 pin=GP18 event=up +2026-05-09T23:32:49 button=9 pin=GP17 event=down +2026-05-09T23:32:49 button=9 pin=GP17 event=up +2026-05-09T23:32:49 button=10 pin=GP16 event=down +2026-05-09T23:32:49 button=10 pin=GP16 event=up +2026-05-09T23:32:50 button=1 pin=GP28 event=down +2026-05-09T23:32:50 button=1 pin=GP28 event=up +2026-05-09T23:32:53 button=7 pin=GP18 event=down +2026-05-09T23:32:53 button=8 pin=GP19 event=down +2026-05-09T23:32:53 button=6 pin=GP20 event=down +2026-05-09T23:32:54 button=6 pin=GP20 event=up +2026-05-09T23:32:54 button=7 pin=GP18 event=up +2026-05-09T23:32:54 button=8 pin=GP19 event=up +2026-05-09T23:32:59 button=9 pin=GP17 event=down +2026-05-09T23:32:59 button=7 pin=GP18 event=down +2026-05-09T23:33:01 button=9 pin=GP17 event=up +2026-05-09T23:33:01 button=7 pin=GP18 event=up +2026-05-09T23:33:01 button=7 pin=GP18 event=down +2026-05-09T23:33:01 button=7 pin=GP18 event=up +2026-05-09T23:33:01 button=7 pin=GP18 event=down +2026-05-09T23:33:01 button=7 pin=GP18 event=up +2026-05-09T23:33:01 button=7 pin=GP18 event=down +2026-05-09T23:33:01 button=7 pin=GP18 event=up +2026-05-09T23:33:01 button=7 pin=GP18 event=down +2026-05-09T23:33:01 button=7 pin=GP18 event=up +2026-05-09T23:33:08 button=2 pin=GP27 event=down +2026-05-09T23:33:08 button=2 pin=GP27 event=up +2026-05-09T23:33:10 button=2 pin=GP27 event=down +2026-05-09T23:33:10 button=8 pin=GP19 event=down +2026-05-09T23:33:10 button=2 pin=GP27 event=up +2026-05-09T23:33:10 button=8 pin=GP19 event=up +2026-05-09T23:33:10 button=8 pin=GP19 event=down +2026-05-09T23:33:11 button=8 pin=GP19 event=up +2026-05-09T23:33:11 button=8 pin=GP19 event=down +2026-05-09T23:33:11 button=8 pin=GP19 event=up +2026-05-09T23:33:18 button=7 pin=GP18 event=down +2026-05-09T23:33:18 button=7 pin=GP18 event=up +2026-05-09T23:33:18 button=7 pin=GP18 event=down +2026-05-09T23:33:18 button=7 pin=GP18 event=up +2026-05-09T23:33:18 button=7 pin=GP18 event=down +2026-05-09T23:33:18 button=7 pin=GP18 event=up +2026-05-09T23:33:18 button=7 pin=GP18 event=down +2026-05-09T23:33:19 button=7 pin=GP18 event=up +2026-05-09T23:33:19 button=7 pin=GP18 event=down +2026-05-09T23:33:19 button=10 pin=GP16 event=down +2026-05-09T23:33:19 button=7 pin=GP18 event=up +2026-05-09T23:33:19 button=2 pin=GP27 event=down +2026-05-09T23:33:19 button=7 pin=GP18 event=down +2026-05-09T23:33:20 button=2 pin=GP27 event=up +2026-05-09T23:33:20 button=10 pin=GP16 event=up +2026-05-09T23:33:20 button=7 pin=GP18 event=up +2026-05-09T23:33:20 button=7 pin=GP18 event=down +2026-05-09T23:33:21 button=7 pin=GP18 event=up +2026-05-09T23:33:21 button=7 pin=GP18 event=down +2026-05-09T23:33:21 button=7 pin=GP18 event=up +2026-05-09T23:33:21 button=7 pin=GP18 event=down +2026-05-09T23:33:22 button=7 pin=GP18 event=up +2026-05-09T23:33:22 button=7 pin=GP18 event=down +2026-05-09T23:33:22 button=7 pin=GP18 event=up +2026-05-09T23:33:22 button=7 pin=GP18 event=down +2026-05-09T23:33:22 button=7 pin=GP18 event=up +2026-05-09T23:33:22 button=7 pin=GP18 event=down +2026-05-09T23:33:22 button=7 pin=GP18 event=up +2026-05-09T23:33:23 button=7 pin=GP18 event=down +2026-05-09T23:33:23 button=7 pin=GP18 event=up +2026-05-09T23:33:28 button=1 pin=GP28 event=down +2026-05-09T23:33:28 button=7 pin=GP18 event=down +2026-05-09T23:33:28 button=9 pin=GP17 event=down +2026-05-09T23:33:28 button=8 pin=GP19 event=down +2026-05-09T23:33:28 button=6 pin=GP20 event=down +2026-05-09T23:33:28 button=10 pin=GP16 event=down +2026-05-09T23:33:29 button=5 pin=GP21 event=down +2026-05-09T23:33:30 button=5 pin=GP21 event=up +2026-05-09T23:33:30 button=10 pin=GP16 event=up +2026-05-09T23:33:31 button=9 pin=GP17 event=up +2026-05-09T23:33:31 button=6 pin=GP20 event=up +2026-05-09T23:33:31 button=7 pin=GP18 event=up +2026-05-09T23:33:31 button=8 pin=GP19 event=up +2026-05-09T23:33:32 button=1 pin=GP28 event=up +2026-05-09T23:33:36 button=7 pin=GP18 event=down +2026-05-09T23:33:36 button=8 pin=GP19 event=down +2026-05-09T23:33:36 button=9 pin=GP17 event=down +2026-05-09T23:33:36 button=6 pin=GP20 event=down +2026-05-09T23:33:36 button=10 pin=GP16 event=down +2026-05-09T23:33:36 button=5 pin=GP21 event=down +2026-05-09T23:33:36 button=5 pin=GP21 event=up +2026-05-09T23:33:36 button=9 pin=GP17 event=up +2026-05-09T23:33:36 button=10 pin=GP16 event=up +2026-05-09T23:33:36 button=7 pin=GP18 event=up +2026-05-09T23:33:36 button=6 pin=GP20 event=up +2026-05-09T23:33:36 button=8 pin=GP19 event=up +2026-05-09T23:33:40 button=7 pin=GP18 event=down +2026-05-09T23:33:40 button=7 pin=GP18 event=up +2026-05-09T23:33:40 button=7 pin=GP18 event=down +2026-05-09T23:33:40 button=7 pin=GP18 event=up +2026-05-09T23:33:40 button=7 pin=GP18 event=down +2026-05-09T23:33:40 button=7 pin=GP18 event=up +2026-05-09T23:33:41 button=7 pin=GP18 event=down +2026-05-09T23:33:41 button=7 pin=GP18 event=up +2026-05-09T23:33:41 button=7 pin=GP18 event=down +2026-05-09T23:33:41 button=7 pin=GP18 event=up +2026-05-09T23:33:41 button=7 pin=GP18 event=down +2026-05-09T23:33:41 button=7 pin=GP18 event=up +2026-05-09T23:33:41 button=7 pin=GP18 event=down +2026-05-09T23:33:42 button=7 pin=GP18 event=up +2026-05-09T23:33:42 button=7 pin=GP18 event=down +2026-05-09T23:33:42 button=7 pin=GP18 event=up +2026-05-09T23:33:42 button=7 pin=GP18 event=down +2026-05-09T23:33:42 button=7 pin=GP18 event=up +2026-05-09T23:33:43 button=7 pin=GP18 event=down +2026-05-09T23:33:43 button=7 pin=GP18 event=up +2026-05-09T23:33:43 button=7 pin=GP18 event=down +2026-05-09T23:33:43 button=7 pin=GP18 event=up +2026-05-09T23:33:43 button=7 pin=GP18 event=down +2026-05-09T23:33:44 button=7 pin=GP18 event=up +2026-05-09T23:33:58 button=7 pin=GP18 event=down +2026-05-09T23:34:05 button=7 pin=GP18 event=up +2026-05-09T23:34:05 button=7 pin=GP18 event=down +2026-05-09T23:34:05 button=7 pin=GP18 event=up +2026-05-09T23:34:06 button=7 pin=GP18 event=down +2026-05-09T23:34:06 button=7 pin=GP18 event=up +2026-05-09T23:34:06 button=7 pin=GP18 event=down +2026-05-09T23:34:06 button=7 pin=GP18 event=up +2026-05-09T23:34:06 button=7 pin=GP18 event=down +2026-05-09T23:34:06 button=7 pin=GP18 event=up +2026-05-09T23:34:06 button=7 pin=GP18 event=down +2026-05-09T23:34:06 button=7 pin=GP18 event=up +2026-05-09T23:34:06 button=7 pin=GP18 event=down +2026-05-09T23:34:07 button=7 pin=GP18 event=up +2026-05-09T23:34:07 button=7 pin=GP18 event=down +2026-05-09T23:34:07 button=7 pin=GP18 event=up +2026-05-09T23:34:07 button=7 pin=GP18 event=down +2026-05-09T23:34:07 button=7 pin=GP18 event=up +2026-05-09T23:34:07 button=7 pin=GP18 event=down +2026-05-09T23:34:07 button=7 pin=GP18 event=up +2026-05-09T23:34:09 button=10 pin=GP16 event=down +2026-05-09T23:34:09 button=10 pin=GP16 event=up +2026-05-09T23:34:09 button=10 pin=GP16 event=down +2026-05-09T23:34:10 button=2 pin=GP27 event=down +2026-05-09T23:34:10 button=1 pin=GP28 event=down +2026-05-09T23:34:10 button=10 pin=GP16 event=up +2026-05-09T23:34:10 button=2 pin=GP27 event=up +2026-05-09T23:34:10 button=1 pin=GP28 event=up +2026-05-09T23:34:10 button=1 pin=GP28 event=down +2026-05-09T23:34:10 button=2 pin=GP27 event=down +2026-05-09T23:34:10 button=1 pin=GP28 event=up +2026-05-09T23:34:11 button=2 pin=GP27 event=up +2026-05-09T23:34:12 button=2 pin=GP27 event=down +2026-05-09T23:34:12 button=2 pin=GP27 event=up +2026-05-09T23:34:13 button=7 pin=GP18 event=down +2026-05-09T23:34:13 button=7 pin=GP18 event=up +2026-05-09T23:34:13 button=7 pin=GP18 event=down +2026-05-09T23:34:13 button=7 pin=GP18 event=up +2026-05-09T23:34:13 button=7 pin=GP18 event=down +2026-05-09T23:34:13 button=7 pin=GP18 event=up +2026-05-09T23:34:14 button=7 pin=GP18 event=down +2026-05-09T23:34:14 button=7 pin=GP18 event=up +2026-05-09T23:34:14 button=7 pin=GP18 event=down +2026-05-09T23:34:14 button=7 pin=GP18 event=up +2026-05-09T23:34:14 button=7 pin=GP18 event=down +2026-05-09T23:34:14 button=7 pin=GP18 event=up +2026-05-09T23:34:15 button=9 pin=GP17 event=down +2026-05-09T23:34:15 button=9 pin=GP17 event=up +2026-05-09T23:34:15 button=9 pin=GP17 event=down +2026-05-09T23:34:15 button=9 pin=GP17 event=up +2026-05-09T23:34:15 button=9 pin=GP17 event=down +2026-05-09T23:34:15 button=9 pin=GP17 event=up +2026-05-09T23:34:15 button=10 pin=GP16 event=down +2026-05-09T23:34:15 button=10 pin=GP16 event=up +2026-05-09T23:34:15 button=10 pin=GP16 event=down +2026-05-09T23:34:15 button=10 pin=GP16 event=up +2026-05-09T23:34:15 button=10 pin=GP16 event=down +2026-05-09T23:34:16 button=10 pin=GP16 event=up +2026-05-09T23:34:16 button=1 pin=GP28 event=down +2026-05-09T23:34:16 button=1 pin=GP28 event=up +2026-05-09T23:34:16 button=1 pin=GP28 event=down +2026-05-09T23:34:16 button=1 pin=GP28 event=up +2026-05-09T23:34:17 button=1 pin=GP28 event=down +2026-05-09T23:34:17 button=1 pin=GP28 event=up +2026-05-09T23:34:17 button=1 pin=GP28 event=down +2026-05-09T23:34:17 button=1 pin=GP28 event=up +2026-05-09T23:34:19 button=8 pin=GP19 event=down +2026-05-09T23:34:19 button=8 pin=GP19 event=up +2026-05-09T23:34:19 button=8 pin=GP19 event=down +2026-05-09T23:34:19 button=8 pin=GP19 event=up +2026-05-09T23:34:19 button=8 pin=GP19 event=down +2026-05-09T23:34:19 button=8 pin=GP19 event=up +2026-05-09T23:34:20 button=6 pin=GP20 event=down +2026-05-09T23:34:20 button=6 pin=GP20 event=up +2026-05-09T23:34:20 button=6 pin=GP20 event=down +2026-05-09T23:34:20 button=6 pin=GP20 event=up +2026-05-09T23:34:20 button=6 pin=GP20 event=down +2026-05-09T23:34:20 button=6 pin=GP20 event=up +2026-05-09T23:34:20 button=5 pin=GP21 event=down +2026-05-09T23:34:20 button=5 pin=GP21 event=up +2026-05-09T23:34:20 button=5 pin=GP21 event=down +2026-05-09T23:34:20 button=5 pin=GP21 event=up +2026-05-09T23:34:20 button=5 pin=GP21 event=down +2026-05-09T23:34:21 button=5 pin=GP21 event=up +2026-05-09T23:34:26 button=5 pin=GP21 event=down +2026-05-09T23:34:27 button=4 pin=GP22 event=down +2026-05-09T23:34:27 button=4 pin=GP22 event=up +2026-05-09T23:34:27 button=4 pin=GP22 event=down +2026-05-09T23:34:27 button=4 pin=GP22 event=up +2026-05-09T23:34:28 button=4 pin=GP22 event=down +2026-05-09T23:34:28 button=4 pin=GP22 event=up +2026-05-09T23:34:29 button=4 pin=GP22 event=down +2026-05-09T23:34:29 button=4 pin=GP22 event=up +2026-05-09T23:34:29 button=2 pin=GP27 event=down +2026-05-09T23:34:29 button=4 pin=GP22 event=down +2026-05-09T23:34:29 button=4 pin=GP22 event=up +2026-05-09T23:34:29 button=2 pin=GP27 event=up +2026-05-09T23:34:29 button=5 pin=GP21 event=up +2026-05-09T23:34:30 button=4 pin=GP22 event=down +2026-05-09T23:34:30 button=4 pin=GP22 event=up +2026-05-09T23:34:30 button=4 pin=GP22 event=down +2026-05-09T23:34:30 button=4 pin=GP22 event=up +2026-05-09T23:34:31 button=4 pin=GP22 event=down +2026-05-09T23:34:31 button=4 pin=GP22 event=up +2026-05-09T23:34:31 button=4 pin=GP22 event=down +2026-05-09T23:34:31 button=4 pin=GP22 event=up +2026-05-09T23:34:31 button=4 pin=GP22 event=down +2026-05-09T23:34:31 button=4 pin=GP22 event=up +2026-05-09T23:34:31 button=4 pin=GP22 event=down +2026-05-09T23:34:31 button=4 pin=GP22 event=up +2026-05-09T23:34:32 button=4 pin=GP22 event=down +2026-05-09T23:34:32 button=4 pin=GP22 event=up +2026-05-09T23:34:32 button=4 pin=GP22 event=down +2026-05-09T23:34:32 button=4 pin=GP22 event=up +2026-05-09T23:34:32 button=4 pin=GP22 event=down +2026-05-09T23:34:33 button=4 pin=GP22 event=up +2026-05-09T23:34:33 button=4 pin=GP22 event=down +2026-05-09T23:34:33 button=4 pin=GP22 event=up +2026-05-09T23:34:33 button=3 pin=GP26 event=down +2026-05-09T23:34:33 button=3 pin=GP26 event=up +2026-05-09T23:34:33 button=3 pin=GP26 event=down +2026-05-09T23:34:33 button=3 pin=GP26 event=up +2026-05-09T23:34:34 button=3 pin=GP26 event=down +2026-05-09T23:34:34 button=3 pin=GP26 event=up +2026-05-09T23:34:34 button=3 pin=GP26 event=down +2026-05-09T23:34:34 button=3 pin=GP26 event=up +2026-05-09T23:34:34 button=2 pin=GP27 event=down +2026-05-09T23:34:34 button=2 pin=GP27 event=up +2026-05-09T23:34:34 button=2 pin=GP27 event=down +2026-05-09T23:34:34 button=2 pin=GP27 event=up +2026-05-09T23:34:38 button=2 pin=GP27 event=down +2026-05-09T23:34:38 button=2 pin=GP27 event=up +2026-05-09T23:34:42 button=1 pin=GP28 event=down +2026-05-09T23:34:42 button=1 pin=GP28 event=up +2026-05-09T23:34:43 button=10 pin=GP16 event=down +2026-05-09T23:34:43 button=10 pin=GP16 event=up +2026-05-09T23:34:44 button=9 pin=GP17 event=down +2026-05-09T23:34:44 button=9 pin=GP17 event=up +2026-05-09T23:34:44 button=9 pin=GP17 event=down +2026-05-09T23:34:44 button=9 pin=GP17 event=up +2026-05-09T23:34:44 button=9 pin=GP17 event=down +2026-05-09T23:34:45 button=9 pin=GP17 event=up +2026-05-09T23:34:45 button=9 pin=GP17 event=down +2026-05-09T23:34:45 button=9 pin=GP17 event=up +2026-05-09T23:34:45 button=9 pin=GP17 event=down +2026-05-09T23:34:45 button=9 pin=GP17 event=up +2026-05-09T23:34:45 button=9 pin=GP17 event=down +2026-05-09T23:34:45 button=9 pin=GP17 event=up +2026-05-09T23:34:45 button=9 pin=GP17 event=down +2026-05-09T23:34:46 button=9 pin=GP17 event=up +2026-05-09T23:34:46 button=9 pin=GP17 event=down +2026-05-09T23:34:46 button=9 pin=GP17 event=up +2026-05-09T23:34:46 button=9 pin=GP17 event=down +2026-05-09T23:34:46 button=9 pin=GP17 event=up +2026-05-09T23:34:46 button=9 pin=GP17 event=down +2026-05-09T23:34:47 button=9 pin=GP17 event=up +2026-05-09T23:34:47 button=9 pin=GP17 event=down +2026-05-09T23:34:47 button=9 pin=GP17 event=up +2026-05-09T23:34:47 button=7 pin=GP18 event=down +2026-05-09T23:34:47 button=7 pin=GP18 event=up +2026-05-09T23:34:48 button=8 pin=GP19 event=down +2026-05-09T23:34:48 button=8 pin=GP19 event=up +2026-05-09T23:34:48 button=6 pin=GP20 event=down +2026-05-09T23:34:49 button=6 pin=GP20 event=up +2026-05-09T23:34:49 button=5 pin=GP21 event=down +2026-05-09T23:34:49 button=5 pin=GP21 event=up +2026-05-09T23:34:56 button=3 pin=GP26 event=down +2026-05-09T23:34:56 button=3 pin=GP26 event=up +2026-05-09T23:34:56 button=3 pin=GP26 event=down +2026-05-09T23:34:56 button=3 pin=GP26 event=up +2026-05-09T23:34:57 button=2 pin=GP27 event=down +2026-05-09T23:34:57 button=2 pin=GP27 event=up +2026-05-09T23:34:57 button=2 pin=GP27 event=down +2026-05-09T23:34:57 button=2 pin=GP27 event=up +2026-05-09T23:34:57 button=2 pin=GP27 event=down +2026-05-09T23:34:57 button=2 pin=GP27 event=up +2026-05-09T23:35:00 button=4 pin=GP22 event=down +2026-05-09T23:35:00 button=4 pin=GP22 event=up +2026-05-09T23:35:00 button=4 pin=GP22 event=down +2026-05-09T23:35:00 button=4 pin=GP22 event=up +2026-05-09T23:35:23 button=4 pin=GP22 event=down +2026-05-09T23:35:23 button=4 pin=GP22 event=up +2026-05-09T23:35:25 button=4 pin=GP22 event=down +2026-05-09T23:35:25 button=4 pin=GP22 event=up +2026-05-09T23:35:26 button=4 pin=GP22 event=down +2026-05-09T23:35:27 button=4 pin=GP22 event=up +2026-05-09T23:35:27 button=4 pin=GP22 event=down +2026-05-09T23:35:27 button=4 pin=GP22 event=up +2026-05-09T23:35:28 button=4 pin=GP22 event=down +2026-05-09T23:35:28 button=4 pin=GP22 event=up +2026-05-09T23:35:28 button=4 pin=GP22 event=down +2026-05-09T23:35:28 button=4 pin=GP22 event=up +2026-05-09T23:35:28 button=4 pin=GP22 event=down +2026-05-09T23:35:29 button=4 pin=GP22 event=up +2026-05-09T23:35:30 button=4 pin=GP22 event=down +2026-05-09T23:35:30 button=4 pin=GP22 event=up +2026-05-09T23:35:30 button=4 pin=GP22 event=down +2026-05-09T23:35:30 button=4 pin=GP22 event=up +2026-05-09T23:35:30 button=4 pin=GP22 event=down +2026-05-09T23:35:30 button=4 pin=GP22 event=up +2026-05-09T23:35:30 button=4 pin=GP22 event=down +2026-05-09T23:35:31 button=4 pin=GP22 event=up +2026-05-09T23:35:31 button=4 pin=GP22 event=down +2026-05-09T23:35:31 button=4 pin=GP22 event=up +2026-05-09T23:35:35 button=4 pin=GP22 event=down +2026-05-09T23:35:35 button=4 pin=GP22 event=up +2026-05-09T23:35:35 button=4 pin=GP22 event=down +2026-05-09T23:35:36 button=4 pin=GP22 event=up +2026-05-09T23:35:36 button=4 pin=GP22 event=down +2026-05-09T23:35:36 button=4 pin=GP22 event=up +2026-05-09T23:35:36 button=4 pin=GP22 event=down +2026-05-09T23:35:36 button=4 pin=GP22 event=up +2026-05-09T23:35:36 button=4 pin=GP22 event=down +2026-05-09T23:35:36 button=4 pin=GP22 event=up +2026-05-09T23:35:36 button=4 pin=GP22 event=down +2026-05-09T23:35:37 button=4 pin=GP22 event=up +2026-05-09T23:35:37 button=4 pin=GP22 event=down +2026-05-09T23:35:38 button=4 pin=GP22 event=up +2026-05-09T23:35:53 button=4 pin=GP22 event=down +2026-05-09T23:35:54 button=4 pin=GP22 event=up +2026-05-09T23:35:54 button=4 pin=GP22 event=down +2026-05-09T23:35:54 button=4 pin=GP22 event=up +2026-05-09T23:35:54 button=4 pin=GP22 event=down +2026-05-09T23:35:54 button=4 pin=GP22 event=up +2026-05-09T23:35:54 button=4 pin=GP22 event=down +2026-05-09T23:35:54 button=4 pin=GP22 event=up +2026-05-09T23:35:55 button=4 pin=GP22 event=down +2026-05-09T23:35:55 button=4 pin=GP22 event=up +2026-05-09T23:35:56 button=3 pin=GP26 event=down +2026-05-09T23:35:56 button=3 pin=GP26 event=up +2026-05-09T23:35:56 button=3 pin=GP26 event=down +2026-05-09T23:35:56 button=3 pin=GP26 event=up +2026-05-09T23:35:56 button=3 pin=GP26 event=down +2026-05-09T23:35:56 button=3 pin=GP26 event=up +2026-05-09T23:36:03 button=2 pin=GP27 event=down +2026-05-09T23:36:03 button=2 pin=GP27 event=up +2026-05-09T23:36:04 button=2 pin=GP27 event=down +2026-05-09T23:36:04 button=2 pin=GP27 event=up +2026-05-09T23:36:08 button=2 pin=GP27 event=down +2026-05-09T23:36:09 button=2 pin=GP27 event=up +2026-05-09T23:36:10 button=2 pin=GP27 event=down +2026-05-09T23:36:11 button=2 pin=GP27 event=up +2026-05-09T23:36:11 button=2 pin=GP27 event=down +2026-05-09T23:36:11 button=2 pin=GP27 event=up +2026-05-09T23:36:22 button=1 pin=GP28 event=down +2026-05-09T23:36:22 button=1 pin=GP28 event=up +2026-05-09T23:36:22 button=1 pin=GP28 event=down +2026-05-09T23:36:22 button=1 pin=GP28 event=up +2026-05-09T23:36:22 button=1 pin=GP28 event=down +2026-05-09T23:36:23 button=1 pin=GP28 event=up +2026-05-09T23:36:23 button=10 pin=GP16 event=down +2026-05-09T23:36:23 button=10 pin=GP16 event=up +2026-05-09T23:36:23 button=10 pin=GP16 event=down +2026-05-09T23:36:23 button=10 pin=GP16 event=up +2026-05-09T23:36:24 button=9 pin=GP17 event=down +2026-05-09T23:36:25 button=9 pin=GP17 event=up +2026-05-09T23:36:25 button=7 pin=GP18 event=down +2026-05-09T23:36:25 button=7 pin=GP18 event=up +2026-05-09T23:36:26 button=8 pin=GP19 event=down +2026-05-09T23:36:26 button=8 pin=GP19 event=up +2026-05-09T23:36:27 button=6 pin=GP20 event=down +2026-05-09T23:36:27 button=6 pin=GP20 event=up +2026-05-09T23:36:27 button=5 pin=GP21 event=down +2026-05-09T23:36:27 button=5 pin=GP21 event=up +2026-05-09T23:36:28 button=4 pin=GP22 event=down +2026-05-09T23:36:28 button=4 pin=GP22 event=up +2026-05-09T23:36:29 button=3 pin=GP26 event=down +2026-05-09T23:36:29 button=3 pin=GP26 event=up +2026-05-09T23:36:29 button=2 pin=GP27 event=down +2026-05-09T23:36:30 button=2 pin=GP27 event=up +2026-05-09T23:36:31 button=2 pin=GP27 event=down +2026-05-09T23:36:31 button=2 pin=GP27 event=up +2026-05-09T23:36:31 button=2 pin=GP27 event=down +2026-05-09T23:36:31 button=2 pin=GP27 event=up +2026-05-09T23:36:32 button=1 pin=GP28 event=down +2026-05-09T23:36:32 button=1 pin=GP28 event=up +2026-05-09T23:36:33 button=10 pin=GP16 event=down +2026-05-09T23:36:33 button=10 pin=GP16 event=up +2026-05-09T23:36:33 button=9 pin=GP17 event=down +2026-05-09T23:36:33 button=9 pin=GP17 event=up +2026-05-09T23:36:34 button=7 pin=GP18 event=down +2026-05-09T23:36:34 button=7 pin=GP18 event=up +2026-05-09T23:36:34 button=8 pin=GP19 event=down +2026-05-09T23:36:34 button=8 pin=GP19 event=up +2026-05-09T23:36:35 button=6 pin=GP20 event=down +2026-05-09T23:36:35 button=6 pin=GP20 event=up +2026-05-09T23:36:35 button=5 pin=GP21 event=down +2026-05-09T23:36:36 button=5 pin=GP21 event=up +2026-05-09T23:36:37 button=4 pin=GP22 event=down +2026-05-09T23:36:37 button=4 pin=GP22 event=up +2026-05-09T23:36:37 button=4 pin=GP22 event=down +2026-05-09T23:36:37 button=4 pin=GP22 event=up +2026-05-09T23:36:37 button=3 pin=GP26 event=down +2026-05-09T23:36:38 button=3 pin=GP26 event=up +2026-05-09T23:36:38 button=3 pin=GP26 event=down +2026-05-09T23:36:38 button=3 pin=GP26 event=up +2026-05-09T23:36:38 button=2 pin=GP27 event=down +2026-05-09T23:36:39 button=2 pin=GP27 event=up +2026-05-09T23:36:39 button=2 pin=GP27 event=down +2026-05-09T23:36:39 button=2 pin=GP27 event=up +2026-05-09T23:36:39 button=2 pin=GP27 event=down +2026-05-09T23:36:39 button=2 pin=GP27 event=up +2026-05-09T23:36:56 button=1 pin=GP28 event=down +2026-05-09T23:36:56 button=1 pin=GP28 event=up +2026-05-09T23:36:56 button=1 pin=GP28 event=down +2026-05-09T23:36:56 button=1 pin=GP28 event=up +2026-05-09T23:36:56 button=1 pin=GP28 event=down +2026-05-09T23:36:56 button=1 pin=GP28 event=up +2026-05-09T23:36:56 button=10 pin=GP16 event=down +2026-05-09T23:36:56 button=10 pin=GP16 event=up +2026-05-09T23:36:57 button=10 pin=GP16 event=down +2026-05-09T23:36:57 button=10 pin=GP16 event=up +2026-05-09T23:36:57 button=10 pin=GP16 event=down +2026-05-09T23:36:57 button=10 pin=GP16 event=up +2026-05-09T23:36:57 button=10 pin=GP16 event=down +2026-05-09T23:36:57 button=10 pin=GP16 event=up +2026-05-09T23:37:17 button=8 pin=GP19 event=down +2026-05-09T23:37:17 button=8 pin=GP19 event=up +2026-05-09T23:37:17 button=8 pin=GP19 event=down +2026-05-09T23:37:17 button=8 pin=GP19 event=up +2026-05-09T23:37:30 button=8 pin=GP19 event=down +2026-05-09T23:37:30 button=8 pin=GP19 event=up +2026-05-09T23:37:30 button=8 pin=GP19 event=down +2026-05-09T23:37:31 button=8 pin=GP19 event=up +2026-05-09T23:37:31 button=10 pin=GP16 event=down +2026-05-09T23:37:31 button=10 pin=GP16 event=up +2026-05-09T23:37:31 button=10 pin=GP16 event=down +2026-05-09T23:37:31 button=10 pin=GP16 event=up +2026-05-09T23:37:31 button=10 pin=GP16 event=down +2026-05-09T23:37:31 button=10 pin=GP16 event=up +2026-05-09T23:37:34 button=6 pin=GP20 event=down +2026-05-09T23:37:34 button=6 pin=GP20 event=up +2026-05-09T23:37:34 button=6 pin=GP20 event=down +2026-05-09T23:37:34 button=6 pin=GP20 event=up +2026-05-09T23:37:34 button=5 pin=GP21 event=down +2026-05-09T23:37:35 button=5 pin=GP21 event=up +2026-05-09T23:37:35 button=4 pin=GP22 event=down +2026-05-09T23:37:35 button=4 pin=GP22 event=up +2026-05-09T23:37:35 button=4 pin=GP22 event=down +2026-05-09T23:37:35 button=4 pin=GP22 event=up +2026-05-09T23:37:35 button=4 pin=GP22 event=down +2026-05-09T23:37:35 button=4 pin=GP22 event=up +2026-05-09T23:37:35 button=5 pin=GP21 event=down +2026-05-09T23:37:35 button=5 pin=GP21 event=up +2026-05-09T23:37:36 button=5 pin=GP21 event=down +2026-05-09T23:37:36 button=5 pin=GP21 event=up +2026-05-09T23:37:36 button=5 pin=GP21 event=down +2026-05-09T23:37:36 button=5 pin=GP21 event=up +2026-05-09T23:37:36 button=5 pin=GP21 event=down +2026-05-09T23:37:36 button=5 pin=GP21 event=up +2026-05-09T23:37:36 button=5 pin=GP21 event=down +2026-05-09T23:37:36 button=5 pin=GP21 event=up +2026-05-09T23:37:36 button=5 pin=GP21 event=down +2026-05-09T23:37:37 button=5 pin=GP21 event=up +2026-05-09T23:37:37 button=4 pin=GP22 event=down +2026-05-09T23:37:37 button=4 pin=GP22 event=up +2026-05-09T23:37:37 button=4 pin=GP22 event=down +2026-05-09T23:37:37 button=4 pin=GP22 event=up +2026-05-09T23:37:37 button=3 pin=GP26 event=down +2026-05-09T23:37:37 button=3 pin=GP26 event=up +2026-05-09T23:37:37 button=3 pin=GP26 event=down +2026-05-09T23:37:38 button=3 pin=GP26 event=up +2026-05-09T23:37:38 button=3 pin=GP26 event=down +2026-05-09T23:37:38 button=3 pin=GP26 event=up +2026-05-09T23:37:38 button=3 pin=GP26 event=down +2026-05-09T23:37:38 button=3 pin=GP26 event=up +2026-05-09T23:37:39 button=2 pin=GP27 event=down +2026-05-09T23:37:39 button=2 pin=GP27 event=up +2026-05-09T23:37:41 button=10 pin=GP16 event=down +2026-05-09T23:37:41 button=1 pin=GP28 event=down +2026-05-09T23:37:42 button=3 pin=GP26 event=down +2026-05-09T23:37:42 button=3 pin=GP26 event=up +2026-05-09T23:37:42 button=3 pin=GP26 event=down +2026-05-09T23:37:42 button=5 pin=GP21 event=down +2026-05-09T23:37:42 button=4 pin=GP22 event=down +2026-05-09T23:37:42 button=8 pin=GP19 event=down +2026-05-09T23:37:42 button=6 pin=GP20 event=down +2026-05-09T23:37:43 button=6 pin=GP20 event=up +2026-05-09T23:37:43 button=1 pin=GP28 event=up +2026-05-09T23:37:43 button=4 pin=GP22 event=up +2026-05-09T23:37:43 button=5 pin=GP21 event=up +2026-05-09T23:37:43 button=10 pin=GP16 event=up +2026-05-09T23:37:43 button=3 pin=GP26 event=up +2026-05-09T23:37:43 button=8 pin=GP19 event=up +2026-05-09T23:37:43 button=1 pin=GP28 event=down +2026-05-09T23:37:43 button=6 pin=GP20 event=down +2026-05-09T23:37:43 button=8 pin=GP19 event=down +2026-05-09T23:37:43 button=3 pin=GP26 event=down +2026-05-09T23:37:43 button=5 pin=GP21 event=down +2026-05-09T23:37:43 button=4 pin=GP22 event=down +2026-05-09T23:37:43 button=10 pin=GP16 event=down +2026-05-09T23:37:43 button=10 pin=GP16 event=up +2026-05-09T23:37:43 button=4 pin=GP22 event=up +2026-05-09T23:37:43 button=3 pin=GP26 event=up +2026-05-09T23:37:43 button=5 pin=GP21 event=up +2026-05-09T23:37:43 button=1 pin=GP28 event=up +2026-05-09T23:37:43 button=6 pin=GP20 event=up +2026-05-09T23:37:43 button=8 pin=GP19 event=up +2026-05-09T23:37:43 button=6 pin=GP20 event=down +2026-05-09T23:37:43 button=1 pin=GP28 event=down +2026-05-09T23:37:44 button=3 pin=GP26 event=down +2026-05-09T23:37:44 button=5 pin=GP21 event=down +2026-05-09T23:37:44 button=8 pin=GP19 event=down +2026-05-09T23:37:44 button=4 pin=GP22 event=down +2026-05-09T23:37:44 button=4 pin=GP22 event=up +2026-05-09T23:37:44 button=3 pin=GP26 event=up +2026-05-09T23:37:44 button=5 pin=GP21 event=up +2026-05-09T23:37:44 button=6 pin=GP20 event=up +2026-05-09T23:37:44 button=8 pin=GP19 event=up +2026-05-09T23:37:44 button=1 pin=GP28 event=up +2026-05-09T23:37:44 button=1 pin=GP28 event=down +2026-05-09T23:37:44 button=8 pin=GP19 event=down +2026-05-09T23:37:44 button=5 pin=GP21 event=down +2026-05-09T23:37:44 button=3 pin=GP26 event=down +2026-05-09T23:37:44 button=4 pin=GP22 event=down +2026-05-09T23:37:44 button=6 pin=GP20 event=down +2026-05-09T23:37:44 button=1 pin=GP28 event=up +2026-05-09T23:37:44 button=3 pin=GP26 event=up +2026-05-09T23:37:44 button=4 pin=GP22 event=up +2026-05-09T23:37:44 button=5 pin=GP21 event=up +2026-05-09T23:37:44 button=6 pin=GP20 event=up +2026-05-09T23:37:44 button=1 pin=GP28 event=down +2026-05-09T23:37:44 button=3 pin=GP26 event=down +2026-05-09T23:37:44 button=5 pin=GP21 event=down +2026-05-09T23:37:44 button=4 pin=GP22 event=down +2026-05-09T23:37:44 button=6 pin=GP20 event=down +2026-05-09T23:37:44 button=8 pin=GP19 event=up +2026-05-09T23:37:44 button=4 pin=GP22 event=up +2026-05-09T23:37:44 button=3 pin=GP26 event=up +2026-05-09T23:37:44 button=5 pin=GP21 event=up +2026-05-09T23:37:44 button=6 pin=GP20 event=up +2026-05-09T23:37:44 button=8 pin=GP19 event=down +2026-05-09T23:37:44 button=6 pin=GP20 event=down +2026-05-09T23:37:44 button=5 pin=GP21 event=down +2026-05-09T23:37:44 button=3 pin=GP26 event=down +2026-05-09T23:37:44 button=4 pin=GP22 event=down +2026-05-09T23:37:44 button=8 pin=GP19 event=up +2026-05-09T23:37:44 button=1 pin=GP28 event=up +2026-05-09T23:37:44 button=2 pin=GP27 event=down +2026-05-09T23:37:44 button=3 pin=GP26 event=up +2026-05-09T23:37:44 button=4 pin=GP22 event=up +2026-05-09T23:37:44 button=2 pin=GP27 event=up +2026-05-09T23:37:44 button=8 pin=GP19 event=down +2026-05-09T23:37:44 button=1 pin=GP28 event=down +2026-05-09T23:37:44 button=3 pin=GP26 event=down +2026-05-09T23:37:44 button=3 pin=GP26 event=up +2026-05-09T23:37:45 button=6 pin=GP20 event=up +2026-05-09T23:37:45 button=3 pin=GP26 event=down +2026-05-09T23:37:45 button=5 pin=GP21 event=up +2026-05-09T23:37:45 button=3 pin=GP26 event=up +2026-05-09T23:37:45 button=3 pin=GP26 event=down +2026-05-09T23:37:45 button=5 pin=GP21 event=down +2026-05-09T23:37:45 button=6 pin=GP20 event=down +2026-05-09T23:37:45 button=4 pin=GP22 event=down +2026-05-09T23:37:45 button=4 pin=GP22 event=up +2026-05-09T23:37:45 button=3 pin=GP26 event=up +2026-05-09T23:37:45 button=3 pin=GP26 event=down +2026-05-09T23:37:45 button=4 pin=GP22 event=down +2026-05-09T23:37:45 button=4 pin=GP22 event=up +2026-05-09T23:37:45 button=3 pin=GP26 event=up +2026-05-09T23:37:45 button=1 pin=GP28 event=up +2026-05-09T23:37:45 button=5 pin=GP21 event=up +2026-05-09T23:37:45 button=6 pin=GP20 event=up +2026-05-09T23:37:45 button=8 pin=GP19 event=up +2026-05-09T23:37:45 button=1 pin=GP28 event=down +2026-05-09T23:37:46 button=1 pin=GP28 event=up +2026-05-09T23:37:46 button=10 pin=GP16 event=down +2026-05-09T23:37:46 button=10 pin=GP16 event=up +2026-05-09T23:37:56 button=10 pin=GP16 event=down +2026-05-09T23:37:56 button=3 pin=GP26 event=down +2026-05-09T23:37:56 button=1 pin=GP28 event=down +2026-05-09T23:37:56 button=1 pin=GP28 event=up +2026-05-09T23:37:56 button=10 pin=GP16 event=up +2026-05-09T23:37:56 button=3 pin=GP26 event=up +2026-05-09T23:37:58 button=6 pin=GP20 event=down +2026-05-09T23:37:58 button=8 pin=GP19 event=down +2026-05-09T23:37:58 button=1 pin=GP28 event=down +2026-05-09T23:37:58 button=7 pin=GP18 event=down +2026-05-09T23:37:58 button=5 pin=GP21 event=down +2026-05-09T23:37:58 button=10 pin=GP16 event=down +2026-05-09T23:38:01 button=9 pin=GP17 event=down +2026-05-09T23:38:01 button=4 pin=GP22 event=down +2026-05-09T23:38:02 button=4 pin=GP22 event=up +2026-05-09T23:38:02 button=4 pin=GP22 event=down +2026-05-09T23:38:02 button=4 pin=GP22 event=up +2026-05-09T23:38:02 button=10 pin=GP16 event=up +2026-05-09T23:38:02 button=9 pin=GP17 event=up +2026-05-09T23:38:02 button=1 pin=GP28 event=up +2026-05-09T23:38:02 button=7 pin=GP18 event=up +2026-05-09T23:38:02 button=5 pin=GP21 event=up +2026-05-09T23:38:02 button=8 pin=GP19 event=up +2026-05-09T23:38:02 button=6 pin=GP20 event=up +2026-05-09T23:38:06 button=9 pin=GP17 event=down +2026-05-09T23:38:07 button=9 pin=GP17 event=up +2026-05-09T23:38:07 button=9 pin=GP17 event=down +2026-05-09T23:38:07 button=9 pin=GP17 event=up +2026-05-09T23:38:07 button=9 pin=GP17 event=down +2026-05-09T23:38:07 button=9 pin=GP17 event=up +2026-05-09T23:38:08 button=7 pin=GP18 event=down +2026-05-09T23:38:08 button=7 pin=GP18 event=up +2026-05-09T23:38:08 button=7 pin=GP18 event=down +2026-05-09T23:38:08 button=7 pin=GP18 event=up +2026-05-09T23:38:08 button=7 pin=GP18 event=down +2026-05-09T23:38:08 button=7 pin=GP18 event=up +2026-05-09T23:38:24 button=6 pin=GP20 event=down +2026-05-09T23:38:24 button=6 pin=GP20 event=up +2026-05-09T23:38:25 button=6 pin=GP20 event=down +2026-05-09T23:38:25 button=8 pin=GP19 event=down +2026-05-09T23:38:25 button=5 pin=GP21 event=down +2026-05-09T23:38:25 button=5 pin=GP21 event=up +2026-05-09T23:38:25 button=8 pin=GP19 event=up +2026-05-09T23:38:25 button=6 pin=GP20 event=up +2026-05-09T23:38:26 button=1 pin=GP28 event=down +2026-05-09T23:38:26 button=1 pin=GP28 event=up +2026-05-09T23:38:26 button=10 pin=GP16 event=down +2026-05-09T23:38:26 button=10 pin=GP16 event=up +2026-05-09T23:38:26 button=9 pin=GP17 event=down +2026-05-09T23:38:27 button=9 pin=GP17 event=up +2026-05-09T23:38:27 button=9 pin=GP17 event=down +2026-05-09T23:38:27 button=9 pin=GP17 event=up +2026-05-09T23:38:29 button=8 pin=GP19 event=down +2026-05-09T23:38:29 button=8 pin=GP19 event=up +2026-05-09T23:38:30 button=8 pin=GP19 event=down +2026-05-09T23:38:31 button=8 pin=GP19 event=up +2026-05-09T23:38:31 button=8 pin=GP19 event=down +2026-05-09T23:38:31 button=8 pin=GP19 event=up +2026-05-09T23:38:31 button=8 pin=GP19 event=down +2026-05-09T23:38:31 button=8 pin=GP19 event=up +2026-05-09T23:38:31 button=6 pin=GP20 event=down +2026-05-09T23:38:31 button=6 pin=GP20 event=up +2026-05-09T23:38:31 button=6 pin=GP20 event=down +2026-05-09T23:38:31 button=6 pin=GP20 event=up +2026-05-09T23:38:31 button=6 pin=GP20 event=down +2026-05-09T23:38:32 button=6 pin=GP20 event=up +2026-05-09T23:38:32 button=5 pin=GP21 event=down +2026-05-09T23:38:32 button=5 pin=GP21 event=up +2026-05-09T23:38:32 button=5 pin=GP21 event=down +2026-05-09T23:38:32 button=5 pin=GP21 event=up +2026-05-09T23:38:32 button=5 pin=GP21 event=down +2026-05-09T23:38:32 button=5 pin=GP21 event=up +2026-05-09T23:38:32 button=4 pin=GP22 event=down +2026-05-09T23:38:32 button=4 pin=GP22 event=up +2026-05-09T23:38:33 button=4 pin=GP22 event=down +2026-05-09T23:38:33 button=4 pin=GP22 event=up +2026-05-09T23:38:33 button=4 pin=GP22 event=down +2026-05-09T23:38:33 button=4 pin=GP22 event=up +2026-05-09T23:38:33 button=3 pin=GP26 event=down +2026-05-09T23:38:33 button=3 pin=GP26 event=up +2026-05-09T23:38:33 button=3 pin=GP26 event=down +2026-05-09T23:38:34 button=3 pin=GP26 event=up +2026-05-09T23:38:34 button=3 pin=GP26 event=down +2026-05-09T23:38:34 button=3 pin=GP26 event=up +2026-05-09T23:38:34 button=3 pin=GP26 event=down +2026-05-09T23:38:34 button=3 pin=GP26 event=up +2026-05-09T23:38:54 button=3 pin=GP26 event=down +2026-05-09T23:38:54 button=3 pin=GP26 event=up +2026-05-09T23:39:38 button=2 pin=GP27 event=down +2026-05-09T23:39:38 button=2 pin=GP27 event=up +2026-05-09T23:39:38 button=2 pin=GP27 event=down +2026-05-09T23:39:38 button=2 pin=GP27 event=up +2026-05-09T23:39:39 button=2 pin=GP27 event=down +2026-05-09T23:39:39 button=2 pin=GP27 event=up +2026-05-09T23:39:39 button=3 pin=GP26 event=down +2026-05-09T23:39:39 button=3 pin=GP26 event=up +2026-05-09T23:39:40 button=4 pin=GP22 event=down +2026-05-09T23:39:40 button=4 pin=GP22 event=up +2026-05-09T23:39:40 button=5 pin=GP21 event=down +2026-05-09T23:39:40 button=5 pin=GP21 event=up +2026-05-09T23:40:33 button=10 pin=GP16 event=down +2026-05-09T23:40:33 button=10 pin=GP16 event=up +2026-05-09T23:40:33 button=10 pin=GP16 event=down +2026-05-09T23:40:33 button=10 pin=GP16 event=up +2026-05-09T23:40:34 button=10 pin=GP16 event=down +2026-05-09T23:40:34 button=10 pin=GP16 event=up +2026-05-09T23:40:35 button=1 pin=GP28 event=down +2026-05-09T23:40:35 button=1 pin=GP28 event=up +2026-05-09T23:40:35 button=1 pin=GP28 event=down +2026-05-09T23:40:35 button=1 pin=GP28 event=up +2026-05-09T23:40:36 button=1 pin=GP28 event=down +2026-05-09T23:40:36 button=1 pin=GP28 event=up +2026-05-09T23:40:36 button=10 pin=GP16 event=down +2026-05-09T23:40:36 button=10 pin=GP16 event=up +2026-05-09T23:40:36 button=10 pin=GP16 event=down +2026-05-09T23:40:36 button=10 pin=GP16 event=up +2026-05-09T23:40:36 button=10 pin=GP16 event=down +2026-05-09T23:40:36 button=10 pin=GP16 event=up +2026-05-09T23:40:37 button=9 pin=GP17 event=down +2026-05-09T23:40:37 button=9 pin=GP17 event=up +2026-05-09T23:40:37 button=9 pin=GP17 event=down +2026-05-09T23:40:37 button=9 pin=GP17 event=up +2026-05-09T23:40:37 button=9 pin=GP17 event=down +2026-05-09T23:40:37 button=9 pin=GP17 event=up +2026-05-09T23:40:37 button=9 pin=GP17 event=down +2026-05-09T23:40:37 button=9 pin=GP17 event=up +2026-05-09T23:40:37 button=9 pin=GP17 event=down +2026-05-09T23:40:38 button=9 pin=GP17 event=up +2026-05-09T23:40:38 button=7 pin=GP18 event=down +2026-05-09T23:40:38 button=7 pin=GP18 event=up +2026-05-09T23:40:38 button=7 pin=GP18 event=down +2026-05-09T23:40:38 button=7 pin=GP18 event=up +2026-05-09T23:40:38 button=7 pin=GP18 event=down +2026-05-09T23:40:38 button=7 pin=GP18 event=up +2026-05-09T23:40:38 button=7 pin=GP18 event=down +2026-05-09T23:40:39 button=7 pin=GP18 event=up +2026-05-09T23:40:39 button=7 pin=GP18 event=down +2026-05-09T23:40:39 button=7 pin=GP18 event=up +2026-05-09T23:40:39 button=8 pin=GP19 event=down +2026-05-09T23:40:39 button=8 pin=GP19 event=up +2026-05-09T23:40:39 button=8 pin=GP19 event=down +2026-05-09T23:40:40 button=8 pin=GP19 event=up +2026-05-09T23:40:40 button=6 pin=GP20 event=down +2026-05-09T23:40:40 button=6 pin=GP20 event=up +2026-05-09T23:40:40 button=6 pin=GP20 event=down +2026-05-09T23:40:40 button=6 pin=GP20 event=up +2026-05-09T23:40:40 button=6 pin=GP20 event=down +2026-05-09T23:40:41 button=6 pin=GP20 event=up +2026-05-09T23:40:41 button=5 pin=GP21 event=down +2026-05-09T23:40:41 button=5 pin=GP21 event=up +2026-05-09T23:40:41 button=5 pin=GP21 event=down +2026-05-09T23:40:41 button=5 pin=GP21 event=up +2026-05-09T23:40:41 button=5 pin=GP21 event=down +2026-05-09T23:40:42 button=5 pin=GP21 event=up +2026-05-09T23:40:42 button=5 pin=GP21 event=down +2026-05-09T23:40:42 button=5 pin=GP21 event=up +2026-05-09T23:40:42 button=4 pin=GP22 event=down +2026-05-09T23:40:42 button=4 pin=GP22 event=up +2026-05-09T23:40:42 button=4 pin=GP22 event=down +2026-05-09T23:40:42 button=4 pin=GP22 event=up +2026-05-09T23:40:43 button=4 pin=GP22 event=down +2026-05-09T23:40:43 button=4 pin=GP22 event=up +2026-05-09T23:40:43 button=4 pin=GP22 event=down +2026-05-09T23:40:43 button=4 pin=GP22 event=up +2026-05-09T23:40:43 button=3 pin=GP26 event=down +2026-05-09T23:40:43 button=3 pin=GP26 event=up +2026-05-09T23:40:43 button=3 pin=GP26 event=down +2026-05-09T23:40:43 button=3 pin=GP26 event=up +2026-05-09T23:40:44 button=3 pin=GP26 event=down +2026-05-09T23:40:44 button=3 pin=GP26 event=up +2026-05-09T23:40:44 button=3 pin=GP26 event=down +2026-05-09T23:40:44 button=3 pin=GP26 event=up +2026-05-09T23:41:13 button=2 pin=GP27 event=down +2026-05-09T23:41:13 button=2 pin=GP27 event=up +2026-05-09T23:41:13 button=2 pin=GP27 event=down +2026-05-09T23:41:14 button=2 pin=GP27 event=up +2026-05-09T23:42:31 button=7 pin=GP18 event=down +2026-05-09T23:42:31 button=4 pin=GP22 event=down +2026-05-09T23:42:31 button=5 pin=GP21 event=down +2026-05-09T23:42:31 button=1 pin=GP28 event=down +2026-05-09T23:42:31 button=9 pin=GP17 event=down +2026-05-09T23:42:31 button=3 pin=GP26 event=down +2026-05-09T23:42:31 button=3 pin=GP26 event=up +2026-05-09T23:42:31 button=4 pin=GP22 event=up +2026-05-09T23:42:31 button=9 pin=GP17 event=up +2026-05-09T23:42:31 button=7 pin=GP18 event=up +2026-05-09T23:42:31 button=3 pin=GP26 event=down +2026-05-09T23:42:31 button=1 pin=GP28 event=up +2026-05-09T23:42:32 button=5 pin=GP21 event=up +2026-05-09T23:42:32 button=3 pin=GP26 event=up +2026-05-09T23:43:25 button=1 pin=GP28 event=down +2026-05-09T23:43:25 button=1 pin=GP28 event=up +2026-05-09T23:43:25 button=1 pin=GP28 event=down +2026-05-09T23:43:25 button=1 pin=GP28 event=up +2026-05-09T23:43:25 button=1 pin=GP28 event=down +2026-05-09T23:43:25 button=1 pin=GP28 event=up +2026-05-09T23:43:25 button=1 pin=GP28 event=down +2026-05-09T23:43:25 button=1 pin=GP28 event=up +2026-05-09T23:43:26 button=10 pin=GP16 event=down +2026-05-09T23:43:26 button=1 pin=GP28 event=down +2026-05-09T23:43:26 button=1 pin=GP28 event=up +2026-05-09T23:43:26 button=10 pin=GP16 event=up +2026-05-09T23:43:26 button=10 pin=GP16 event=down +2026-05-09T23:43:26 button=10 pin=GP16 event=up +2026-05-09T23:43:26 button=10 pin=GP16 event=down +2026-05-09T23:43:26 button=10 pin=GP16 event=up +2026-05-09T23:43:27 button=9 pin=GP17 event=down +2026-05-09T23:43:27 button=9 pin=GP17 event=up +2026-05-09T23:43:27 button=9 pin=GP17 event=down +2026-05-09T23:43:27 button=9 pin=GP17 event=up +2026-05-09T23:43:27 button=9 pin=GP17 event=down +2026-05-09T23:43:27 button=9 pin=GP17 event=up +2026-05-09T23:43:27 button=9 pin=GP17 event=down +2026-05-09T23:43:27 button=9 pin=GP17 event=up +2026-05-09T23:43:27 button=9 pin=GP17 event=down +2026-05-09T23:43:27 button=9 pin=GP17 event=up +2026-05-09T23:43:27 button=7 pin=GP18 event=down +2026-05-09T23:43:27 button=7 pin=GP18 event=up +2026-05-09T23:43:27 button=7 pin=GP18 event=down +2026-05-09T23:43:28 button=7 pin=GP18 event=up +2026-05-09T23:43:28 button=7 pin=GP18 event=down +2026-05-09T23:43:28 button=7 pin=GP18 event=up +2026-05-09T23:43:28 button=7 pin=GP18 event=down +2026-05-09T23:43:28 button=7 pin=GP18 event=up +2026-05-09T23:43:28 button=7 pin=GP18 event=down +2026-05-09T23:43:28 button=7 pin=GP18 event=up +2026-05-09T23:43:28 button=8 pin=GP19 event=down +2026-05-09T23:43:29 button=8 pin=GP19 event=up +2026-05-09T23:43:29 button=8 pin=GP19 event=down +2026-05-09T23:43:29 button=8 pin=GP19 event=up +2026-05-09T23:43:29 button=8 pin=GP19 event=down +2026-05-09T23:43:29 button=8 pin=GP19 event=up +2026-05-09T23:43:29 button=8 pin=GP19 event=down +2026-05-09T23:43:29 button=8 pin=GP19 event=up +2026-05-09T23:43:29 button=8 pin=GP19 event=down +2026-05-09T23:43:29 button=8 pin=GP19 event=up +2026-05-09T23:43:29 button=6 pin=GP20 event=down +2026-05-09T23:43:29 button=6 pin=GP20 event=up +2026-05-09T23:43:29 button=6 pin=GP20 event=down +2026-05-09T23:43:30 button=6 pin=GP20 event=up +2026-05-09T23:43:30 button=6 pin=GP20 event=down +2026-05-09T23:43:30 button=6 pin=GP20 event=up +2026-05-09T23:43:31 button=5 pin=GP21 event=down +2026-05-09T23:43:31 button=5 pin=GP21 event=up +2026-05-09T23:43:31 button=5 pin=GP21 event=down +2026-05-09T23:43:31 button=5 pin=GP21 event=up +2026-05-09T23:43:31 button=5 pin=GP21 event=down +2026-05-09T23:43:31 button=5 pin=GP21 event=up +2026-05-09T23:43:31 button=5 pin=GP21 event=down +2026-05-09T23:43:31 button=5 pin=GP21 event=up +2026-05-09T23:43:31 button=5 pin=GP21 event=down +2026-05-09T23:43:31 button=5 pin=GP21 event=up +2026-05-09T23:43:32 button=4 pin=GP22 event=down +2026-05-09T23:43:32 button=4 pin=GP22 event=up +2026-05-09T23:43:32 button=4 pin=GP22 event=down +2026-05-09T23:43:32 button=4 pin=GP22 event=up +2026-05-09T23:43:32 button=4 pin=GP22 event=down +2026-05-09T23:43:32 button=4 pin=GP22 event=up +2026-05-09T23:43:32 button=4 pin=GP22 event=down +2026-05-09T23:43:32 button=4 pin=GP22 event=up +2026-05-09T23:43:32 button=4 pin=GP22 event=down +2026-05-09T23:43:32 button=4 pin=GP22 event=up +2026-05-09T23:43:33 button=3 pin=GP26 event=down +2026-05-09T23:43:33 button=3 pin=GP26 event=up +2026-05-09T23:43:33 button=3 pin=GP26 event=down +2026-05-09T23:43:33 button=3 pin=GP26 event=up +2026-05-09T23:43:33 button=3 pin=GP26 event=down +2026-05-09T23:43:33 button=3 pin=GP26 event=up +2026-05-09T23:43:33 button=3 pin=GP26 event=down +2026-05-09T23:43:33 button=3 pin=GP26 event=up +2026-05-09T23:43:33 button=3 pin=GP26 event=down +2026-05-09T23:43:33 button=3 pin=GP26 event=up +2026-05-09T23:48:49 button=7 pin=GP18 event=down +2026-05-09T23:48:49 button=7 pin=GP18 event=up +2026-05-09T23:48:49 button=7 pin=GP18 event=down +2026-05-09T23:48:49 button=7 pin=GP18 event=up +2026-05-09T23:48:49 button=1 pin=GP28 event=down +2026-05-09T23:48:49 button=7 pin=GP18 event=down +2026-05-09T23:48:49 button=1 pin=GP28 event=up +2026-05-09T23:48:49 button=10 pin=GP16 event=down +2026-05-09T23:48:49 button=7 pin=GP18 event=up +2026-05-09T23:48:49 button=1 pin=GP28 event=down +2026-05-09T23:48:49 button=7 pin=GP18 event=down +2026-05-09T23:48:49 button=10 pin=GP16 event=up +2026-05-09T23:48:49 button=10 pin=GP16 event=down +2026-05-09T23:48:49 button=7 pin=GP18 event=up +2026-05-09T23:48:49 button=1 pin=GP28 event=up +2026-05-09T23:48:50 button=7 pin=GP18 event=down +2026-05-09T23:48:50 button=10 pin=GP16 event=up +2026-05-09T23:48:50 button=10 pin=GP16 event=down +2026-05-09T23:48:50 button=1 pin=GP28 event=down +2026-05-09T23:48:50 button=7 pin=GP18 event=up +2026-05-09T23:48:50 button=1 pin=GP28 event=up +2026-05-09T23:48:50 button=10 pin=GP16 event=up +2026-05-09T23:48:50 button=7 pin=GP18 event=down +2026-05-09T23:48:50 button=10 pin=GP16 event=down +2026-05-09T23:48:50 button=7 pin=GP18 event=up +2026-05-09T23:48:50 button=10 pin=GP16 event=up +2026-05-09T23:50:31 button=7 pin=GP18 event=down +2026-05-09T23:50:31 button=8 pin=GP19 event=down +2026-05-09T23:50:31 button=7 pin=GP18 event=up +2026-05-09T23:50:31 button=8 pin=GP19 event=up +2026-05-09T23:56:03 button=6 pin=GP20 event=down +2026-05-09T23:56:03 button=6 pin=GP20 event=up +2026-05-09T23:56:03 button=6 pin=GP20 event=down +2026-05-09T23:56:03 button=6 pin=GP20 event=up +2026-05-09T23:56:03 button=6 pin=GP20 event=down +2026-05-09T23:56:03 button=6 pin=GP20 event=up +2026-05-09T23:56:04 button=6 pin=GP20 event=down +2026-05-09T23:56:04 button=6 pin=GP20 event=up +2026-05-09T23:56:04 button=6 pin=GP20 event=down +2026-05-09T23:56:04 button=6 pin=GP20 event=up +2026-05-09T23:56:04 button=6 pin=GP20 event=down +2026-05-09T23:56:05 button=6 pin=GP20 event=up +2026-05-09T23:56:05 button=6 pin=GP20 event=down +2026-05-09T23:56:05 button=6 pin=GP20 event=up +2026-05-09T23:56:05 button=5 pin=GP21 event=down +2026-05-09T23:56:05 button=5 pin=GP21 event=up +2026-05-09T23:56:05 button=5 pin=GP21 event=down +2026-05-09T23:56:05 button=5 pin=GP21 event=up +2026-05-09T23:56:06 button=5 pin=GP21 event=down +2026-05-09T23:56:06 button=5 pin=GP21 event=up +2026-05-09T23:56:06 button=5 pin=GP21 event=down +2026-05-09T23:56:06 button=5 pin=GP21 event=up +2026-05-09T23:56:06 button=6 pin=GP20 event=down +2026-05-09T23:56:06 button=6 pin=GP20 event=up +2026-05-09T23:56:06 button=6 pin=GP20 event=down +2026-05-09T23:56:07 button=6 pin=GP20 event=up +2026-05-09T23:56:07 button=6 pin=GP20 event=down +2026-05-09T23:56:07 button=6 pin=GP20 event=up +2026-05-09T23:56:07 button=6 pin=GP20 event=down +2026-05-09T23:56:07 button=6 pin=GP20 event=up +2026-05-09T23:56:07 button=6 pin=GP20 event=down +2026-05-09T23:56:07 button=6 pin=GP20 event=up +2026-05-09T23:56:08 button=6 pin=GP20 event=down +2026-05-09T23:56:08 button=6 pin=GP20 event=up +2026-05-09T23:56:08 button=6 pin=GP20 event=down +2026-05-09T23:56:08 button=6 pin=GP20 event=up +2026-05-09T23:56:08 button=6 pin=GP20 event=down +2026-05-09T23:56:08 button=6 pin=GP20 event=up +2026-05-09T23:56:08 button=6 pin=GP20 event=down +2026-05-09T23:56:08 button=6 pin=GP20 event=up +2026-05-09T23:56:09 button=6 pin=GP20 event=down +2026-05-09T23:56:09 button=6 pin=GP20 event=up +2026-05-09T23:56:09 button=6 pin=GP20 event=down +2026-05-09T23:56:09 button=6 pin=GP20 event=up +2026-05-09T23:56:09 button=6 pin=GP20 event=down +2026-05-09T23:56:09 button=6 pin=GP20 event=up +2026-05-09T23:56:09 button=6 pin=GP20 event=down +2026-05-09T23:56:09 button=6 pin=GP20 event=up +2026-05-09T23:56:57 button=1 pin=GP28 event=down +2026-05-09T23:56:57 button=1 pin=GP28 event=up +2026-05-09T23:56:57 button=1 pin=GP28 event=down +2026-05-09T23:56:57 button=1 pin=GP28 event=up +2026-05-09T23:56:57 button=1 pin=GP28 event=down +2026-05-09T23:56:57 button=1 pin=GP28 event=up +2026-05-09T23:56:57 button=1 pin=GP28 event=down +2026-05-09T23:56:57 button=1 pin=GP28 event=up +2026-05-09T23:56:57 button=1 pin=GP28 event=down +2026-05-09T23:56:58 button=1 pin=GP28 event=up +2026-05-09T23:56:58 button=1 pin=GP28 event=down +2026-05-09T23:56:58 button=1 pin=GP28 event=up +2026-05-09T23:56:58 button=1 pin=GP28 event=down +2026-05-09T23:56:58 button=1 pin=GP28 event=up +2026-05-09T23:56:58 button=1 pin=GP28 event=down +2026-05-09T23:56:58 button=1 pin=GP28 event=up +2026-05-09T23:56:58 button=1 pin=GP28 event=down +2026-05-09T23:56:58 button=1 pin=GP28 event=up +2026-05-09T23:56:58 button=1 pin=GP28 event=down +2026-05-09T23:56:58 button=1 pin=GP28 event=up +2026-05-09T23:56:59 button=1 pin=GP28 event=down +2026-05-09T23:56:59 button=1 pin=GP28 event=up +2026-05-09T23:56:59 button=1 pin=GP28 event=down +2026-05-09T23:56:59 button=1 pin=GP28 event=up +2026-05-09T23:56:59 button=1 pin=GP28 event=down +2026-05-09T23:56:59 button=1 pin=GP28 event=up +2026-05-09T23:56:59 button=1 pin=GP28 event=down +2026-05-09T23:56:59 button=1 pin=GP28 event=up +2026-05-09T23:56:59 button=1 pin=GP28 event=down +2026-05-09T23:56:59 button=1 pin=GP28 event=up +2026-05-09T23:56:59 button=1 pin=GP28 event=down +2026-05-09T23:57:00 button=1 pin=GP28 event=up +2026-05-09T23:57:00 button=1 pin=GP28 event=down +2026-05-09T23:57:00 button=1 pin=GP28 event=up +2026-05-09T23:57:00 button=1 pin=GP28 event=down +2026-05-09T23:57:00 button=1 pin=GP28 event=up +2026-05-09T23:57:00 button=1 pin=GP28 event=down +2026-05-09T23:57:00 button=1 pin=GP28 event=up +2026-05-09T23:57:02 button=1 pin=GP28 event=down +2026-05-09T23:57:02 button=1 pin=GP28 event=up +2026-05-09T23:57:02 button=1 pin=GP28 event=down +2026-05-09T23:57:02 button=1 pin=GP28 event=up +2026-05-09T23:57:02 button=1 pin=GP28 event=down +2026-05-09T23:57:02 button=1 pin=GP28 event=up +2026-05-09T23:57:02 button=1 pin=GP28 event=down +2026-05-09T23:57:02 button=1 pin=GP28 event=up +2026-05-09T23:57:02 button=1 pin=GP28 event=down +2026-05-09T23:57:02 button=1 pin=GP28 event=up +2026-05-09T23:57:02 button=1 pin=GP28 event=down +2026-05-09T23:57:02 button=1 pin=GP28 event=up +2026-05-09T23:57:02 button=1 pin=GP28 event=down +2026-05-09T23:57:03 button=1 pin=GP28 event=up +2026-05-09T23:57:03 button=1 pin=GP28 event=down +2026-05-09T23:57:03 button=1 pin=GP28 event=up +2026-05-09T23:57:03 button=1 pin=GP28 event=down +2026-05-09T23:57:03 button=1 pin=GP28 event=up +2026-05-09T23:57:03 button=1 pin=GP28 event=down +2026-05-09T23:57:03 button=1 pin=GP28 event=up +2026-05-09T23:57:03 button=1 pin=GP28 event=down +2026-05-09T23:57:03 button=1 pin=GP28 event=up +2026-05-09T23:57:03 button=1 pin=GP28 event=down +2026-05-09T23:57:03 button=1 pin=GP28 event=up +2026-05-09T23:57:03 button=1 pin=GP28 event=down +2026-05-09T23:57:03 button=1 pin=GP28 event=up +2026-05-09T23:57:04 button=1 pin=GP28 event=down +2026-05-09T23:57:04 button=1 pin=GP28 event=up +2026-05-09T23:57:04 button=1 pin=GP28 event=down +2026-05-09T23:57:04 button=1 pin=GP28 event=up +2026-05-09T23:57:04 button=1 pin=GP28 event=down +2026-05-09T23:57:04 button=1 pin=GP28 event=up +2026-05-09T23:57:04 button=1 pin=GP28 event=down +2026-05-09T23:57:04 button=1 pin=GP28 event=up +2026-05-09T23:57:04 button=1 pin=GP28 event=down +2026-05-09T23:57:05 button=1 pin=GP28 event=up +2026-05-09T23:57:06 button=1 pin=GP28 event=down +2026-05-09T23:57:06 button=1 pin=GP28 event=up +2026-05-09T23:57:06 button=1 pin=GP28 event=down +2026-05-09T23:57:06 button=1 pin=GP28 event=up +2026-05-09T23:57:06 button=1 pin=GP28 event=down +2026-05-09T23:57:06 button=1 pin=GP28 event=up +2026-05-09T23:57:06 button=1 pin=GP28 event=down +2026-05-09T23:57:06 button=1 pin=GP28 event=up +2026-05-09T23:57:43 button=3 pin=GP26 event=down +2026-05-09T23:57:43 button=3 pin=GP26 event=up +2026-05-09T23:57:43 button=3 pin=GP26 event=down +2026-05-09T23:57:43 button=3 pin=GP26 event=up +2026-05-09T23:57:43 button=3 pin=GP26 event=down +2026-05-09T23:57:43 button=3 pin=GP26 event=up +2026-05-09T23:57:43 button=3 pin=GP26 event=down +2026-05-09T23:57:43 button=3 pin=GP26 event=up +2026-05-09T23:57:43 button=3 pin=GP26 event=down +2026-05-09T23:57:43 button=3 pin=GP26 event=up +2026-05-09T23:57:44 button=3 pin=GP26 event=down +2026-05-09T23:57:44 button=3 pin=GP26 event=up +2026-05-09T23:57:45 button=3 pin=GP26 event=down +2026-05-09T23:57:45 button=3 pin=GP26 event=up +2026-05-09T23:57:45 button=3 pin=GP26 event=down +2026-05-09T23:57:45 button=3 pin=GP26 event=up +2026-05-09T23:57:45 button=3 pin=GP26 event=down +2026-05-09T23:57:45 button=3 pin=GP26 event=up +2026-05-09T23:57:46 button=3 pin=GP26 event=down +2026-05-09T23:57:46 button=3 pin=GP26 event=up +2026-05-09T23:57:46 button=3 pin=GP26 event=down +2026-05-09T23:57:46 button=3 pin=GP26 event=up +2026-05-09T23:57:47 button=3 pin=GP26 event=down +2026-05-09T23:57:47 button=3 pin=GP26 event=up +2026-05-09T23:57:50 button=8 pin=GP19 event=down +2026-05-09T23:57:50 button=8 pin=GP19 event=up +2026-05-09T23:57:50 button=6 pin=GP20 event=down +2026-05-09T23:57:50 button=6 pin=GP20 event=up +2026-05-09T23:57:50 button=7 pin=GP18 event=down +2026-05-09T23:57:50 button=7 pin=GP18 event=up +2026-05-09T23:57:51 button=8 pin=GP19 event=down +2026-05-09T23:57:51 button=6 pin=GP20 event=down +2026-05-09T23:57:51 button=7 pin=GP18 event=down +2026-05-09T23:57:51 button=8 pin=GP19 event=up +2026-05-09T23:57:51 button=6 pin=GP20 event=up +2026-05-09T23:57:51 button=8 pin=GP19 event=down +2026-05-09T23:57:51 button=6 pin=GP20 event=down +2026-05-09T23:57:51 button=7 pin=GP18 event=up +2026-05-09T23:57:51 button=6 pin=GP20 event=up +2026-05-09T23:57:51 button=8 pin=GP19 event=up +2026-05-09T23:58:41 button=5 pin=GP21 event=down +2026-05-09T23:58:41 button=8 pin=GP19 event=down +2026-05-09T23:58:41 button=5 pin=GP21 event=up +2026-05-09T23:58:41 button=8 pin=GP19 event=up +2026-05-09T23:58:41 button=8 pin=GP19 event=down +2026-05-09T23:58:41 button=8 pin=GP19 event=up +2026-05-09T23:58:42 button=8 pin=GP19 event=down +2026-05-09T23:58:42 button=8 pin=GP19 event=up +2026-05-09T23:58:42 button=8 pin=GP19 event=down +2026-05-09T23:58:42 button=8 pin=GP19 event=up +2026-05-09T23:58:43 button=8 pin=GP19 event=down +2026-05-09T23:58:43 button=8 pin=GP19 event=up +2026-05-09T23:58:43 button=8 pin=GP19 event=down +2026-05-09T23:58:43 button=8 pin=GP19 event=up +2026-05-09T23:58:43 button=8 pin=GP19 event=down +2026-05-09T23:58:43 button=8 pin=GP19 event=up +2026-05-10T10:38:00 button=1 pin=GP28 event=down +2026-05-10T10:38:01 button=1 pin=GP28 event=up +2026-05-10T10:38:01 button=10 pin=GP16 event=down +2026-05-10T10:38:01 button=10 pin=GP16 event=up +2026-05-10T10:38:11 button=9 pin=GP17 event=down +2026-05-10T10:38:11 button=9 pin=GP17 event=up +2026-05-10T10:38:11 button=9 pin=GP17 event=down +2026-05-10T10:38:11 button=9 pin=GP17 event=up +2026-05-10T10:38:11 button=9 pin=GP17 event=down +2026-05-10T10:38:11 button=9 pin=GP17 event=up +2026-05-10T10:38:11 button=9 pin=GP17 event=down +2026-05-10T10:38:11 button=9 pin=GP17 event=up +2026-05-10T10:38:12 button=9 pin=GP17 event=down +2026-05-10T10:38:12 button=9 pin=GP17 event=up +2026-05-10T10:38:13 button=9 pin=GP17 event=down +2026-05-10T10:38:13 button=9 pin=GP17 event=up +2026-05-10T10:38:13 button=9 pin=GP17 event=down +2026-05-10T10:38:13 button=9 pin=GP17 event=up +2026-05-10T10:38:14 button=7 pin=GP18 event=down +2026-05-10T10:38:14 button=7 pin=GP18 event=up +2026-05-10T10:38:15 button=8 pin=GP19 event=down +2026-05-10T10:38:15 button=8 pin=GP19 event=up +2026-05-10T10:38:15 button=6 pin=GP20 event=down +2026-05-10T10:38:16 button=6 pin=GP20 event=up +2026-05-10T10:38:16 button=5 pin=GP21 event=down +2026-05-10T10:38:16 button=5 pin=GP21 event=up +2026-05-10T10:38:16 button=4 pin=GP22 event=down +2026-05-10T10:38:17 button=4 pin=GP22 event=up +2026-05-10T10:38:17 button=3 pin=GP26 event=down +2026-05-10T10:38:17 button=3 pin=GP26 event=up +2026-05-10T10:38:20 button=9 pin=GP17 event=down +2026-05-10T10:38:20 button=9 pin=GP17 event=up +2026-05-10T10:38:20 button=9 pin=GP17 event=down +2026-05-10T10:38:20 button=9 pin=GP17 event=up +2026-05-10T10:38:20 button=9 pin=GP17 event=down +2026-05-10T10:38:20 button=9 pin=GP17 event=up +2026-05-10T10:38:21 button=9 pin=GP17 event=down +2026-05-10T10:38:21 button=9 pin=GP17 event=up +2026-05-10T10:38:21 button=9 pin=GP17 event=down +2026-05-10T10:38:22 button=9 pin=GP17 event=up +2026-05-10T10:38:22 button=9 pin=GP17 event=down +2026-05-10T10:38:22 button=9 pin=GP17 event=up +2026-05-10T10:38:22 button=9 pin=GP17 event=down +2026-05-10T10:38:23 button=9 pin=GP17 event=up +2026-05-10T10:38:23 button=9 pin=GP17 event=down +2026-05-10T10:38:23 button=9 pin=GP17 event=up +2026-05-10T10:38:26 button=9 pin=GP17 event=down +2026-05-10T10:38:26 button=9 pin=GP17 event=up +2026-05-10T10:38:26 button=9 pin=GP17 event=down +2026-05-10T10:38:26 button=9 pin=GP17 event=up +2026-05-10T10:38:27 button=9 pin=GP17 event=down +2026-05-10T10:38:27 button=9 pin=GP17 event=up +2026-05-10T10:38:27 button=9 pin=GP17 event=down +2026-05-10T10:38:27 button=9 pin=GP17 event=up +2026-05-10T10:38:27 button=9 pin=GP17 event=down +2026-05-10T10:38:27 button=9 pin=GP17 event=up +2026-05-10T10:38:27 button=9 pin=GP17 event=down +2026-05-10T10:38:27 button=9 pin=GP17 event=up +2026-05-10T10:38:28 button=9 pin=GP17 event=down +2026-05-10T10:38:28 button=9 pin=GP17 event=up +2026-05-10T10:38:28 button=9 pin=GP17 event=down +2026-05-10T10:38:28 button=9 pin=GP17 event=up +2026-05-10T10:38:28 button=9 pin=GP17 event=down +2026-05-10T10:38:28 button=9 pin=GP17 event=up +2026-05-10T10:38:28 button=9 pin=GP17 event=down +2026-05-10T10:38:28 button=9 pin=GP17 event=up +2026-05-10T10:38:28 button=9 pin=GP17 event=down +2026-05-10T10:38:28 button=9 pin=GP17 event=up +2026-05-10T10:38:29 button=1 pin=GP28 event=down +2026-05-10T10:38:29 button=1 pin=GP28 event=up +2026-05-10T10:38:29 button=10 pin=GP16 event=down +2026-05-10T10:38:29 button=10 pin=GP16 event=up +2026-05-10T10:38:29 button=9 pin=GP17 event=down +2026-05-10T10:38:29 button=9 pin=GP17 event=up +2026-05-10T10:38:30 button=7 pin=GP18 event=down +2026-05-10T10:38:30 button=7 pin=GP18 event=up +2026-05-10T10:38:30 button=8 pin=GP19 event=down +2026-05-10T10:38:30 button=8 pin=GP19 event=up +2026-05-10T10:38:31 button=6 pin=GP20 event=down +2026-05-10T10:38:31 button=6 pin=GP20 event=up +2026-05-10T10:38:31 button=5 pin=GP21 event=down +2026-05-10T10:38:31 button=5 pin=GP21 event=up +2026-05-10T10:38:31 button=4 pin=GP22 event=down +2026-05-10T10:38:32 button=4 pin=GP22 event=up +2026-05-10T10:38:32 button=3 pin=GP26 event=down +2026-05-10T10:38:32 button=3 pin=GP26 event=up +2026-05-10T10:42:04 button=3 pin=GP26 event=down +2026-05-10T10:42:04 button=3 pin=GP26 event=up +2026-05-10T10:43:18 button=5 pin=GP21 event=down +2026-05-10T10:43:18 button=5 pin=GP21 event=up +2026-05-10T10:43:18 button=2 pin=GP27 event=down +2026-05-10T10:43:18 button=2 pin=GP27 event=up +2026-05-10T10:43:19 button=3 pin=GP26 event=down +2026-05-10T10:43:20 button=3 pin=GP26 event=up +2026-05-10T10:43:54 button=3 pin=GP26 event=down +2026-05-10T10:43:54 button=6 pin=GP20 event=down +2026-05-10T10:43:55 button=6 pin=GP20 event=up +2026-05-10T10:43:55 button=3 pin=GP26 event=up +2026-05-10T10:46:49 button=2 pin=GP27 event=down +2026-05-10T10:46:50 button=2 pin=GP27 event=up +2026-05-10T10:46:50 button=2 pin=GP27 event=down +2026-05-10T10:46:50 button=2 pin=GP27 event=up +2026-05-10T10:46:50 button=2 pin=GP27 event=down +2026-05-10T10:46:50 button=2 pin=GP27 event=up +2026-05-10T10:46:51 button=2 pin=GP27 event=down +2026-05-10T10:46:51 button=2 pin=GP27 event=up +2026-05-10T10:46:51 button=2 pin=GP27 event=down +2026-05-10T10:46:51 button=2 pin=GP27 event=up +2026-05-10T10:51:00 button=2 pin=GP27 event=down +2026-05-10T10:51:00 button=2 pin=GP27 event=up +2026-05-10T10:51:01 button=2 pin=GP27 event=down +2026-05-10T10:51:01 button=2 pin=GP27 event=up +2026-05-10T10:51:01 button=2 pin=GP27 event=down +2026-05-10T10:51:01 button=2 pin=GP27 event=up +2026-05-10T10:51:01 button=2 pin=GP27 event=down +2026-05-10T10:51:01 button=2 pin=GP27 event=up +2026-05-10T10:51:27 button=1 pin=GP28 event=down +2026-05-10T10:51:27 button=1 pin=GP28 event=up +2026-05-10T10:51:28 button=10 pin=GP16 event=down +2026-05-10T10:51:28 button=10 pin=GP16 event=up +2026-05-10T10:51:28 button=9 pin=GP17 event=down +2026-05-10T10:51:28 button=9 pin=GP17 event=up +2026-05-10T10:51:29 button=7 pin=GP18 event=down +2026-05-10T10:51:29 button=7 pin=GP18 event=up +2026-05-10T10:51:29 button=8 pin=GP19 event=down +2026-05-10T10:51:29 button=8 pin=GP19 event=up +2026-05-10T10:51:29 button=6 pin=GP20 event=down +2026-05-10T10:51:30 button=6 pin=GP20 event=up +2026-05-10T10:51:30 button=5 pin=GP21 event=down +2026-05-10T10:51:30 button=5 pin=GP21 event=up +2026-05-10T10:51:30 button=4 pin=GP22 event=down +2026-05-10T10:51:30 button=4 pin=GP22 event=up +2026-05-10T10:51:31 button=3 pin=GP26 event=down +2026-05-10T10:51:31 button=3 pin=GP26 event=up +2026-05-10T10:51:39 button=2 pin=GP27 event=down +2026-05-10T10:51:40 button=2 pin=GP27 event=up +2026-05-10T10:51:40 button=2 pin=GP27 event=down +2026-05-10T10:51:40 button=2 pin=GP27 event=up +2026-05-10T10:51:40 button=2 pin=GP27 event=down +2026-05-10T10:51:40 button=2 pin=GP27 event=up +2026-05-10T10:51:40 button=2 pin=GP27 event=down +2026-05-10T10:51:41 button=2 pin=GP27 event=up +2026-05-10T10:51:41 button=2 pin=GP27 event=down +2026-05-10T10:51:41 button=2 pin=GP27 event=up +2026-05-10T10:53:03 button=2 pin=GP27 event=down +2026-05-10T10:53:03 button=2 pin=GP27 event=up +2026-05-10T10:53:03 button=2 pin=GP27 event=down +2026-05-10T10:53:03 button=2 pin=GP27 event=up +2026-05-10T10:53:18 button=9 pin=GP17 event=down +2026-05-10T10:53:18 button=9 pin=GP17 event=up +2026-05-10T10:53:18 button=9 pin=GP17 event=down +2026-05-10T10:53:18 button=9 pin=GP17 event=up +2026-05-10T10:53:19 button=9 pin=GP17 event=down +2026-05-10T10:53:19 button=9 pin=GP17 event=up +2026-05-10T10:53:19 button=9 pin=GP17 event=down +2026-05-10T10:53:19 button=9 pin=GP17 event=up +2026-05-10T10:53:19 button=9 pin=GP17 event=down +2026-05-10T10:53:19 button=9 pin=GP17 event=up +2026-05-10T10:53:19 button=9 pin=GP17 event=down +2026-05-10T10:53:19 button=9 pin=GP17 event=up +2026-05-10T10:54:47 button=4 pin=GP22 event=down +2026-05-10T10:54:47 button=3 pin=GP26 event=down +2026-05-10T10:54:47 button=5 pin=GP21 event=down +2026-05-10T10:54:48 button=3 pin=GP26 event=up +2026-05-10T10:54:48 button=4 pin=GP22 event=up +2026-05-10T10:54:49 button=6 pin=GP20 event=down +2026-05-10T10:54:49 button=6 pin=GP20 event=up +2026-05-10T10:54:49 button=5 pin=GP21 event=up +2026-05-10T10:54:49 button=5 pin=GP21 event=down +2026-05-10T10:54:50 button=5 pin=GP21 event=up +2026-05-10T10:59:06 button=5 pin=GP21 event=down +2026-05-10T10:59:08 button=5 pin=GP21 event=up +2026-05-10T10:59:24 button=3 pin=GP26 event=down +2026-05-10T10:59:24 button=3 pin=GP26 event=up +2026-05-10T10:59:24 button=3 pin=GP26 event=down +2026-05-10T10:59:29 button=4 pin=GP22 event=down +2026-05-10T10:59:30 button=4 pin=GP22 event=up +2026-05-10T10:59:30 button=4 pin=GP22 event=down +2026-05-10T10:59:30 button=4 pin=GP22 event=up +2026-05-10T10:59:33 button=4 pin=GP22 event=down +2026-05-10T10:59:34 button=5 pin=GP21 event=down +2026-05-10T10:59:36 button=6 pin=GP20 event=down +2026-05-10T10:59:37 button=5 pin=GP21 event=up +2026-05-10T10:59:37 button=5 pin=GP21 event=down +2026-05-10T10:59:38 button=8 pin=GP19 event=down +2026-05-10T10:59:50 button=8 pin=GP19 event=up +2026-05-10T10:59:50 button=6 pin=GP20 event=up +2026-05-10T10:59:50 button=5 pin=GP21 event=up +2026-05-10T10:59:51 button=3 pin=GP26 event=up +2026-05-10T10:59:51 button=4 pin=GP22 event=up +2026-05-10T11:00:03 button=3 pin=GP26 event=down +2026-05-10T11:00:04 button=5 pin=GP21 event=down +2026-05-10T11:00:04 button=3 pin=GP26 event=up +2026-05-10T11:00:04 button=3 pin=GP26 event=down +2026-05-10T11:00:05 button=3 pin=GP26 event=up +2026-05-10T11:00:06 button=6 pin=GP20 event=down +2026-05-10T11:00:06 button=3 pin=GP26 event=down +2026-05-10T11:00:07 button=4 pin=GP22 event=down +2026-05-10T11:00:08 button=4 pin=GP22 event=up +2026-05-10T11:00:08 button=3 pin=GP26 event=up +2026-05-10T11:00:08 button=6 pin=GP20 event=up +2026-05-10T11:00:08 button=5 pin=GP21 event=up +2026-05-10T11:00:30 button=5 pin=GP21 event=down +2026-05-10T11:00:31 button=5 pin=GP21 event=up +2026-05-10T11:00:35 button=5 pin=GP21 event=down +2026-05-10T11:00:37 button=5 pin=GP21 event=up +2026-05-10T11:00:51 button=3 pin=GP26 event=down +2026-05-10T11:00:53 button=4 pin=GP22 event=down +2026-05-10T11:00:53 button=5 pin=GP21 event=down +2026-05-10T11:00:53 button=6 pin=GP20 event=down +2026-05-10T11:00:54 button=6 pin=GP20 event=up +2026-05-10T11:00:54 button=6 pin=GP20 event=down +2026-05-10T11:00:57 button=8 pin=GP19 event=down +2026-05-10T11:00:57 button=8 pin=GP19 event=up +2026-05-10T11:00:58 button=6 pin=GP20 event=up +2026-05-10T11:00:58 button=4 pin=GP22 event=up +2026-05-10T11:00:58 button=5 pin=GP21 event=up +2026-05-10T11:00:58 button=3 pin=GP26 event=up +2026-05-10T11:01:06 button=3 pin=GP26 event=down +2026-05-10T11:01:07 button=5 pin=GP21 event=down +2026-05-10T11:01:08 button=4 pin=GP22 event=down +2026-05-10T11:01:08 button=4 pin=GP22 event=up +2026-05-10T11:01:10 button=6 pin=GP20 event=down +2026-05-10T11:01:11 button=4 pin=GP22 event=down +2026-05-10T11:01:11 button=4 pin=GP22 event=up +2026-05-10T11:01:11 button=4 pin=GP22 event=down +2026-05-10T11:01:14 button=8 pin=GP19 event=down +2026-05-10T11:01:29 button=8 pin=GP19 event=up +2026-05-10T11:01:29 button=4 pin=GP22 event=up +2026-05-10T11:01:29 button=5 pin=GP21 event=up +2026-05-10T11:01:29 button=6 pin=GP20 event=up +2026-05-10T11:01:29 button=3 pin=GP26 event=up +2026-05-10T11:01:38 button=5 pin=GP21 event=down +2026-05-10T11:01:39 button=5 pin=GP21 event=up +2026-05-10T11:01:39 button=3 pin=GP26 event=down +2026-05-10T11:01:39 button=4 pin=GP22 event=down +2026-05-10T11:01:40 button=5 pin=GP21 event=down +2026-05-10T11:01:40 button=5 pin=GP21 event=up +2026-05-10T11:01:40 button=4 pin=GP22 event=up +2026-05-10T11:01:40 button=3 pin=GP26 event=up +2026-05-10T11:01:41 button=6 pin=GP20 event=down +2026-05-10T11:01:41 button=5 pin=GP21 event=down +2026-05-10T11:01:41 button=3 pin=GP26 event=down +2026-05-10T11:01:41 button=8 pin=GP19 event=down +2026-05-10T11:01:41 button=4 pin=GP22 event=down +2026-05-10T11:01:41 button=7 pin=GP18 event=down +2026-05-10T11:01:41 button=7 pin=GP18 event=up +2026-05-10T11:01:41 button=8 pin=GP19 event=up +2026-05-10T11:01:41 button=6 pin=GP20 event=up +2026-05-10T11:01:41 button=4 pin=GP22 event=up +2026-05-10T11:01:41 button=5 pin=GP21 event=up +2026-05-10T11:01:41 button=3 pin=GP26 event=up +2026-05-10T11:01:41 button=3 pin=GP26 event=down +2026-05-10T11:01:41 button=4 pin=GP22 event=down +2026-05-10T11:01:41 button=5 pin=GP21 event=down +2026-05-10T11:01:41 button=6 pin=GP20 event=down +2026-05-10T11:01:41 button=8 pin=GP19 event=down +2026-05-10T11:01:41 button=8 pin=GP19 event=up +2026-05-10T11:01:41 button=3 pin=GP26 event=up +2026-05-10T11:01:41 button=4 pin=GP22 event=up +2026-05-10T11:01:41 button=5 pin=GP21 event=up +2026-05-10T11:01:41 button=6 pin=GP20 event=up +2026-05-10T11:01:41 button=3 pin=GP26 event=down +2026-05-10T11:01:41 button=4 pin=GP22 event=down +2026-05-10T11:01:41 button=5 pin=GP21 event=down +2026-05-10T11:01:41 button=6 pin=GP20 event=down +2026-05-10T11:01:41 button=8 pin=GP19 event=down +2026-05-10T11:01:41 button=7 pin=GP18 event=down +2026-05-10T11:01:41 button=7 pin=GP18 event=up +2026-05-10T11:01:42 button=8 pin=GP19 event=up +2026-05-10T11:01:42 button=3 pin=GP26 event=up +2026-05-10T11:01:42 button=4 pin=GP22 event=up +2026-05-10T11:01:42 button=5 pin=GP21 event=up +2026-05-10T11:01:42 button=6 pin=GP20 event=up +2026-05-10T11:02:02 button=3 pin=GP26 event=down +2026-05-10T11:02:02 button=4 pin=GP22 event=down +2026-05-10T11:02:03 button=5 pin=GP21 event=down +2026-05-10T11:02:03 button=5 pin=GP21 event=up +2026-05-10T11:02:03 button=3 pin=GP26 event=up +2026-05-10T11:02:04 button=3 pin=GP26 event=down +2026-05-10T11:02:04 button=5 pin=GP21 event=down +2026-05-10T11:02:05 button=5 pin=GP21 event=up +2026-05-10T11:02:05 button=5 pin=GP21 event=down +2026-05-10T11:02:05 button=5 pin=GP21 event=up +2026-05-10T11:02:05 button=5 pin=GP21 event=down +2026-05-10T11:02:05 button=3 pin=GP26 event=up +2026-05-10T11:02:05 button=5 pin=GP21 event=up +2026-05-10T11:02:06 button=3 pin=GP26 event=down +2026-05-10T11:02:06 button=5 pin=GP21 event=down +2026-05-10T11:02:07 button=5 pin=GP21 event=up +2026-05-10T11:02:07 button=3 pin=GP26 event=up +2026-05-10T11:02:07 button=4 pin=GP22 event=up +2026-05-10T11:02:09 button=3 pin=GP26 event=down +2026-05-10T11:02:09 button=4 pin=GP22 event=down +2026-05-10T11:02:09 button=5 pin=GP21 event=down +2026-05-10T11:02:09 button=5 pin=GP21 event=up +2026-05-10T11:02:10 button=5 pin=GP21 event=down +2026-05-10T11:02:14 button=6 pin=GP20 event=down +2026-05-10T11:02:20 button=6 pin=GP20 event=up +2026-05-10T11:02:20 button=4 pin=GP22 event=up +2026-05-10T11:02:20 button=5 pin=GP21 event=up +2026-05-10T11:02:20 button=3 pin=GP26 event=up +2026-05-10T11:02:28 button=3 pin=GP26 event=down +2026-05-10T11:02:35 button=4 pin=GP22 event=down +2026-05-10T11:02:35 button=5 pin=GP21 event=down +2026-05-10T11:02:36 button=5 pin=GP21 event=up +2026-05-10T11:02:36 button=4 pin=GP22 event=up +2026-05-10T11:02:37 button=3 pin=GP26 event=up +2026-05-10T11:02:37 button=3 pin=GP26 event=down +2026-05-10T11:02:38 button=3 pin=GP26 event=up +2026-05-10T11:02:38 button=3 pin=GP26 event=down +2026-05-10T11:02:40 button=4 pin=GP22 event=down +2026-05-10T11:02:40 button=5 pin=GP21 event=down +2026-05-10T11:02:41 button=5 pin=GP21 event=up +2026-05-10T11:02:41 button=4 pin=GP22 event=up +2026-05-10T11:02:41 button=3 pin=GP26 event=up +2026-05-10T11:11:18 button=3 pin=GP26 event=down +2026-05-10T11:11:19 button=3 pin=GP26 event=up +2026-05-10T11:11:24 button=3 pin=GP26 event=down +2026-05-10T11:11:24 button=3 pin=GP26 event=up +2026-05-10T11:11:25 button=3 pin=GP26 event=down +2026-05-10T11:11:26 button=3 pin=GP26 event=up +2026-05-10T11:12:49 button=2 pin=GP27 event=down +2026-05-10T11:12:49 button=2 pin=GP27 event=up +2026-05-10T11:12:50 button=2 pin=GP27 event=down +2026-05-10T11:12:50 button=2 pin=GP27 event=up +2026-05-10T11:12:50 button=2 pin=GP27 event=down +2026-05-10T11:12:50 button=2 pin=GP27 event=up +2026-05-10T11:12:50 button=2 pin=GP27 event=down +2026-05-10T11:12:50 button=2 pin=GP27 event=up +2026-05-10T11:12:50 button=3 pin=GP26 event=down +2026-05-10T11:12:51 button=3 pin=GP26 event=up +2026-05-10T11:12:51 button=4 pin=GP22 event=down +2026-05-10T11:12:52 button=4 pin=GP22 event=up +2026-05-10T11:12:52 button=5 pin=GP21 event=down +2026-05-10T11:12:52 button=5 pin=GP21 event=up +2026-05-10T11:12:52 button=6 pin=GP20 event=down +2026-05-10T11:12:52 button=6 pin=GP20 event=up +2026-05-10T11:12:53 button=8 pin=GP19 event=down +2026-05-10T11:12:53 button=8 pin=GP19 event=up +2026-05-10T11:12:53 button=7 pin=GP18 event=down +2026-05-10T11:12:53 button=7 pin=GP18 event=up +2026-05-10T11:12:54 button=9 pin=GP17 event=down +2026-05-10T11:12:54 button=9 pin=GP17 event=up +2026-05-10T11:12:55 button=10 pin=GP16 event=down +2026-05-10T11:12:55 button=10 pin=GP16 event=up +2026-05-10T11:12:55 button=1 pin=GP28 event=down +2026-05-10T11:12:55 button=1 pin=GP28 event=up +2026-05-10T11:13:24 button=2 pin=GP27 event=down +2026-05-10T11:13:24 button=2 pin=GP27 event=up +2026-05-10T11:13:24 button=5 pin=GP21 event=down +2026-05-10T11:13:25 button=2 pin=GP27 event=down +2026-05-10T11:13:25 button=5 pin=GP21 event=up +2026-05-10T11:13:27 button=2 pin=GP27 event=up +2026-05-10T11:13:28 button=2 pin=GP27 event=down +2026-05-10T11:13:28 button=2 pin=GP27 event=up +2026-05-10T11:13:30 button=2 pin=GP27 event=down +2026-05-10T11:13:31 button=2 pin=GP27 event=up +2026-05-10T11:13:31 button=10 pin=GP16 event=down +2026-05-10T11:13:31 button=2 pin=GP27 event=down +2026-05-10T11:13:31 button=10 pin=GP16 event=up +2026-05-10T11:13:32 button=2 pin=GP27 event=up +2026-05-10T11:13:32 button=2 pin=GP27 event=down +2026-05-10T11:13:33 button=2 pin=GP27 event=up +2026-05-10T11:13:34 button=2 pin=GP27 event=down +2026-05-10T11:13:37 button=2 pin=GP27 event=up +2026-05-10T11:13:38 button=2 pin=GP27 event=down +2026-05-10T11:13:41 button=2 pin=GP27 event=up +2026-05-10T11:13:41 button=2 pin=GP27 event=down +2026-05-10T11:13:41 button=2 pin=GP27 event=up +2026-05-10T11:13:42 button=2 pin=GP27 event=down +2026-05-10T11:13:42 button=2 pin=GP27 event=up +2026-05-10T11:13:42 button=2 pin=GP27 event=down +2026-05-10T11:13:42 button=2 pin=GP27 event=up +2026-05-10T11:13:42 button=2 pin=GP27 event=down +2026-05-10T11:13:42 button=2 pin=GP27 event=up +2026-05-10T11:13:42 button=2 pin=GP27 event=down +2026-05-10T11:13:42 button=2 pin=GP27 event=up +2026-05-10T11:13:42 button=2 pin=GP27 event=down +2026-05-10T11:13:43 button=2 pin=GP27 event=up +2026-05-10T11:13:48 button=2 pin=GP27 event=down +2026-05-10T11:13:48 button=2 pin=GP27 event=up +2026-05-10T11:13:48 button=3 pin=GP26 event=down +2026-05-10T11:13:48 button=3 pin=GP26 event=up +2026-05-10T11:13:49 button=4 pin=GP22 event=down +2026-05-10T11:13:49 button=4 pin=GP22 event=up +2026-05-10T11:13:49 button=5 pin=GP21 event=down +2026-05-10T11:13:49 button=5 pin=GP21 event=up +2026-05-10T11:13:50 button=6 pin=GP20 event=down +2026-05-10T11:13:50 button=6 pin=GP20 event=up +2026-05-10T11:13:50 button=8 pin=GP19 event=down +2026-05-10T11:13:50 button=8 pin=GP19 event=up +2026-05-10T11:13:51 button=7 pin=GP18 event=down +2026-05-10T11:13:51 button=7 pin=GP18 event=up +2026-05-10T11:13:51 button=9 pin=GP17 event=down +2026-05-10T11:13:51 button=9 pin=GP17 event=up +2026-05-10T11:13:52 button=10 pin=GP16 event=down +2026-05-10T11:13:52 button=10 pin=GP16 event=up +2026-05-10T11:13:52 button=1 pin=GP28 event=down +2026-05-10T11:13:53 button=1 pin=GP28 event=up +2026-05-10T11:18:18 button=8 pin=GP19 event=down +2026-05-10T11:18:18 button=8 pin=GP19 event=up +2026-05-10T11:19:30 button=1 pin=GP28 event=down +2026-05-10T11:19:30 button=1 pin=GP28 event=up +2026-05-10T11:19:31 button=10 pin=GP16 event=down +2026-05-10T11:19:31 button=10 pin=GP16 event=up +2026-05-10T11:19:31 button=9 pin=GP17 event=down +2026-05-10T11:19:31 button=9 pin=GP17 event=up +2026-05-10T11:19:32 button=7 pin=GP18 event=down +2026-05-10T11:19:32 button=7 pin=GP18 event=up +2026-05-10T11:19:32 button=8 pin=GP19 event=down +2026-05-10T11:19:32 button=8 pin=GP19 event=up +2026-05-10T11:19:32 button=6 pin=GP20 event=down +2026-05-10T11:19:33 button=6 pin=GP20 event=up +2026-05-10T11:19:33 button=5 pin=GP21 event=down +2026-05-10T11:19:33 button=5 pin=GP21 event=up +2026-05-10T11:19:33 button=4 pin=GP22 event=down +2026-05-10T11:19:34 button=4 pin=GP22 event=up +2026-05-10T11:19:34 button=3 pin=GP26 event=down +2026-05-10T11:19:34 button=3 pin=GP26 event=up +2026-05-10T11:19:34 button=2 pin=GP27 event=down +2026-05-10T11:19:34 button=2 pin=GP27 event=up +2026-05-10T11:35:38 button=9 pin=GP17 event=down +2026-05-10T11:35:38 button=9 pin=GP17 event=up +2026-05-10T11:35:38 button=9 pin=GP17 event=down +2026-05-10T11:35:38 button=9 pin=GP17 event=up diff --git a/exports/box.stl b/exports/box.stl new file mode 100644 index 0000000..9598ea4 --- /dev/null +++ b/exports/box.stl @@ -0,0 +1,702 @@ +solid ASCII + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex -1.410000e+02 -9.000000e+00 2.000000e+00 + vertex -1.434998e+02 2.535000e+01 2.000000e+00 + vertex -1.409998e+02 -9.000000e+00 2.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 2.000000e+00 + vertex -1.434998e+02 2.535000e+01 2.000000e+00 + vertex 9.150000e+01 2.535000e+01 2.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 2.000000e+00 + vertex 9.150000e+01 2.535000e+01 2.000000e+00 + vertex 9.150000e+01 -1.235000e+01 2.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex -1.410000e+02 -1.235000e+01 2.000000e+00 + vertex -1.434998e+02 -1.235000e+01 2.000000e+00 + vertex -1.410000e+02 -9.000000e+00 2.000000e+00 + endloop + endfacet + facet normal -0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex -1.410000e+02 -9.000000e+00 2.000000e+00 + vertex -1.434998e+02 -1.235000e+01 2.000000e+00 + vertex -1.434998e+02 2.535000e+01 2.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex 9.150000e+01 -1.235000e+01 2.000000e+00 + vertex -1.409998e+02 -1.235000e+01 2.000000e+00 + vertex -1.409998e+02 -9.000000e+00 2.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 5.000000e+00 + vertex 8.900000e+01 -9.000000e+00 5.000000e+00 + vertex -1.409998e+02 2.285000e+01 5.000000e+00 + endloop + endfacet + facet normal -0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -1.409998e+02 2.285000e+01 5.000000e+00 + vertex 8.900000e+01 -9.000000e+00 5.000000e+00 + vertex 8.900000e+01 2.285000e+01 5.000000e+00 + endloop + endfacet + facet normal -0.000000e+00 -0.000000e+00 -1.000000e+00 + outer loop + vertex -1.409998e+02 1.215000e+01 1.270000e+01 + vertex -1.409998e+02 8.499998e-01 1.270000e+01 + vertex -1.434998e+02 1.215000e+01 1.270000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex -1.434998e+02 1.215000e+01 1.270000e+01 + vertex -1.409998e+02 8.499998e-01 1.270000e+01 + vertex -1.434998e+02 8.499998e-01 1.270000e+01 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 1.215000e+01 6.000000e+00 + vertex -1.409998e+02 1.215000e+01 1.270000e+01 + vertex -1.434998e+02 1.215000e+01 6.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 0.000000e+00 + outer loop + vertex -1.434998e+02 1.215000e+01 6.000000e+00 + vertex -1.409998e+02 1.215000e+01 1.270000e+01 + vertex -1.434998e+02 1.215000e+01 1.270000e+01 + endloop + endfacet + facet normal 0.000000e+00 -0.000000e+00 1.000000e+00 + outer loop + vertex -1.409998e+02 8.499998e-01 6.000000e+00 + vertex -1.409998e+02 1.215000e+01 6.000000e+00 + vertex -1.434998e+02 8.499998e-01 6.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -1.434998e+02 8.499998e-01 6.000000e+00 + vertex -1.409998e+02 1.215000e+01 6.000000e+00 + vertex -1.434998e+02 1.215000e+01 6.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 8.499998e-01 1.270000e+01 + vertex -1.409998e+02 8.499998e-01 6.000000e+00 + vertex -1.434998e+02 8.499998e-01 1.270000e+01 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex -1.434998e+02 8.499998e-01 1.270000e+01 + vertex -1.409998e+02 8.499998e-01 6.000000e+00 + vertex -1.434998e+02 8.499998e-01 6.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 8.600016e+01 1.885000e+01 3.748780e+01 + vertex 8.600016e+01 -5.000000e+00 3.748780e+01 + vertex 8.900000e+01 -9.000000e+00 3.748780e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 8.900000e+01 -9.000000e+00 3.748780e+01 + vertex 8.600016e+01 -5.000000e+00 3.748780e+01 + vertex -1.379998e+02 -5.000000e+00 3.748780e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 8.900000e+01 -9.000000e+00 3.748780e+01 + vertex -1.379998e+02 -5.000000e+00 3.748780e+01 + vertex -1.409998e+02 -9.000000e+00 3.748780e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 3.748780e+01 + vertex -1.379998e+02 -5.000000e+00 3.748780e+01 + vertex -1.379998e+02 1.885000e+01 3.748780e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 3.748780e+01 + vertex -1.379998e+02 1.885000e+01 3.748780e+01 + vertex -1.409998e+02 2.285000e+01 3.748780e+01 + endloop + endfacet + facet normal -0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -1.409998e+02 2.285000e+01 3.748780e+01 + vertex -1.379998e+02 1.885000e+01 3.748780e+01 + vertex 8.900000e+01 2.285000e+01 3.748780e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 8.900000e+01 2.285000e+01 3.748780e+01 + vertex -1.379998e+02 1.885000e+01 3.748780e+01 + vertex 8.600016e+01 1.885000e+01 3.748780e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 8.900000e+01 2.285000e+01 3.748780e+01 + vertex 8.600016e+01 1.885000e+01 3.748780e+01 + vertex 8.900000e+01 -9.000000e+00 3.748780e+01 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex 8.600016e+01 -5.000000e+00 3.748780e+01 + vertex 8.600016e+01 -5.000000e+00 3.652193e+01 + vertex -1.379998e+02 -5.000000e+00 3.748780e+01 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex -1.379998e+02 -5.000000e+00 3.748780e+01 + vertex 8.600016e+01 -5.000000e+00 3.652193e+01 + vertex -1.379998e+02 -5.000000e+00 3.652193e+01 + endloop + endfacet + facet normal -0.000000e+00 -1.000000e+00 -0.000000e+00 + outer loop + vertex 8.600016e+01 1.885000e+01 3.748780e+01 + vertex -1.379998e+02 1.885000e+01 3.748780e+01 + vertex 8.600016e+01 1.885000e+01 3.652193e+01 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 0.000000e+00 + outer loop + vertex 8.600016e+01 1.885000e+01 3.652193e+01 + vertex -1.379998e+02 1.885000e+01 3.748780e+01 + vertex -1.379998e+02 1.885000e+01 3.652193e+01 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 -0.000000e+00 + outer loop + vertex -1.379998e+02 1.885000e+01 3.652193e+01 + vertex -1.379998e+02 1.885000e+01 3.748780e+01 + vertex -1.379998e+02 -5.000000e+00 3.652193e+01 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.379998e+02 -5.000000e+00 3.652193e+01 + vertex -1.379998e+02 1.885000e+01 3.748780e+01 + vertex -1.379998e+02 -5.000000e+00 3.748780e+01 + endloop + endfacet + facet normal -1.000000e+00 -0.000000e+00 -0.000000e+00 + outer loop + vertex 8.600016e+01 1.885000e+01 3.748780e+01 + vertex 8.600016e+01 1.885000e+01 3.652193e+01 + vertex 8.600016e+01 -5.000000e+00 3.748780e+01 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex 8.600016e+01 -5.000000e+00 3.748780e+01 + vertex 8.600016e+01 1.885000e+01 3.652193e+01 + vertex 8.600016e+01 -5.000000e+00 3.652193e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex 8.900000e+01 -9.000000e+00 3.652193e+01 + vertex 8.600016e+01 1.885000e+01 3.652193e+01 + vertex 8.900000e+01 2.285000e+01 3.652193e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex 8.900000e+01 2.285000e+01 3.652193e+01 + vertex 8.600016e+01 1.885000e+01 3.652193e+01 + vertex -1.379998e+02 1.885000e+01 3.652193e+01 + endloop + endfacet + facet normal -0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex 8.900000e+01 2.285000e+01 3.652193e+01 + vertex -1.379998e+02 1.885000e+01 3.652193e+01 + vertex -1.409998e+02 2.285000e+01 3.652193e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex -1.409998e+02 2.285000e+01 3.652193e+01 + vertex -1.379998e+02 1.885000e+01 3.652193e+01 + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + vertex -1.379998e+02 1.885000e+01 3.652193e+01 + vertex -1.379998e+02 -5.000000e+00 3.652193e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + vertex -1.379998e+02 -5.000000e+00 3.652193e+01 + vertex 8.900000e+01 -9.000000e+00 3.652193e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex 8.900000e+01 -9.000000e+00 3.652193e+01 + vertex -1.379998e+02 -5.000000e+00 3.652193e+01 + vertex 8.600016e+01 -5.000000e+00 3.652193e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex 8.900000e+01 -9.000000e+00 3.652193e+01 + vertex 8.600016e+01 -5.000000e+00 3.652193e+01 + vertex 8.600016e+01 1.885000e+01 3.652193e+01 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 -0.000000e+00 + outer loop + vertex 8.900000e+01 -9.000000e+00 3.748780e+01 + vertex -1.409998e+02 -9.000000e+00 3.748780e+01 + vertex 8.900000e+01 -9.000000e+00 4.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex 8.900000e+01 -9.000000e+00 4.000000e+01 + vertex -1.409998e+02 -9.000000e+00 3.748780e+01 + vertex -1.409998e+02 -9.000000e+00 4.000000e+01 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex 8.900000e+01 2.285000e+01 3.748780e+01 + vertex 8.900000e+01 -9.000000e+00 3.748780e+01 + vertex 8.900000e+01 2.285000e+01 4.000000e+01 + endloop + endfacet + facet normal -1.000000e+00 -0.000000e+00 0.000000e+00 + outer loop + vertex 8.900000e+01 2.285000e+01 4.000000e+01 + vertex 8.900000e+01 -9.000000e+00 3.748780e+01 + vertex 8.900000e+01 -9.000000e+00 4.000000e+01 + endloop + endfacet + facet normal -1.000000e+00 -0.000000e+00 0.000000e+00 + outer loop + vertex 8.900000e+01 2.285000e+01 3.652193e+01 + vertex 8.900000e+01 2.285000e+01 5.000000e+00 + vertex 8.900000e+01 -9.000000e+00 3.652193e+01 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex 8.900000e+01 -9.000000e+00 3.652193e+01 + vertex 8.900000e+01 2.285000e+01 5.000000e+00 + vertex 8.900000e+01 -9.000000e+00 5.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 1.181809e-08 + outer loop + vertex 8.900000e+01 2.285000e+01 3.652193e+01 + vertex -1.409998e+02 2.285000e+01 3.652193e+01 + vertex 8.900000e+01 2.285000e+01 5.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 1.181809e-08 + outer loop + vertex 8.900000e+01 2.285000e+01 5.000000e+00 + vertex -1.409998e+02 2.285000e+01 3.652193e+01 + vertex -1.409998e+02 2.285000e+01 5.000000e+00 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 2.285000e+01 3.748780e+01 + vertex -1.409998e+02 2.285000e+01 4.000000e+01 + vertex -1.409998e+02 -9.000000e+00 3.748780e+01 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 3.748780e+01 + vertex -1.409998e+02 2.285000e+01 4.000000e+01 + vertex -1.409998e+02 -9.000000e+00 4.000000e+01 + endloop + endfacet + facet normal -0.000000e+00 -1.000000e+00 -0.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 4.000000e+01 + vertex -1.410000e+02 -9.000000e+00 4.000000e+01 + vertex -1.409998e+02 -9.000000e+00 3.748780e+01 + endloop + endfacet + facet normal -0.000000e+00 -1.000000e+00 -0.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 3.748780e+01 + vertex -1.410000e+02 -9.000000e+00 4.000000e+01 + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + endloop + endfacet + facet normal -0.000000e+00 -1.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + vertex -1.410000e+02 -9.000000e+00 4.000000e+01 + vertex -1.410000e+02 -9.000000e+00 2.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 -0.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + vertex -1.410000e+02 -9.000000e+00 2.000000e+00 + vertex -1.409998e+02 -9.000000e+00 5.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 -0.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 5.000000e+00 + vertex -1.410000e+02 -9.000000e+00 2.000000e+00 + vertex -1.409998e+02 -9.000000e+00 2.000000e+00 + endloop + endfacet + facet normal 1.000000e+00 -0.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 8.499998e-01 6.000000e+00 + vertex -1.409998e+02 -9.000000e+00 5.000000e+00 + vertex -1.409998e+02 1.215000e+01 6.000000e+00 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 1.215000e+01 6.000000e+00 + vertex -1.409998e+02 -9.000000e+00 5.000000e+00 + vertex -1.409998e+02 2.285000e+01 5.000000e+00 + endloop + endfacet + facet normal 1.000000e+00 -0.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 1.215000e+01 6.000000e+00 + vertex -1.409998e+02 2.285000e+01 5.000000e+00 + vertex -1.409998e+02 1.215000e+01 1.270000e+01 + endloop + endfacet + facet normal 1.000000e+00 -0.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 1.215000e+01 1.270000e+01 + vertex -1.409998e+02 2.285000e+01 5.000000e+00 + vertex -1.409998e+02 2.285000e+01 3.652193e+01 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 -0.000000e+00 + outer loop + vertex -1.409998e+02 1.215000e+01 1.270000e+01 + vertex -1.409998e+02 2.285000e+01 3.652193e+01 + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 -0.000000e+00 + outer loop + vertex -1.409998e+02 8.499998e-01 6.000000e+00 + vertex -1.409998e+02 8.499998e-01 1.270000e+01 + vertex -1.409998e+02 -9.000000e+00 5.000000e+00 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 5.000000e+00 + vertex -1.409998e+02 8.499998e-01 1.270000e+01 + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + vertex -1.409998e+02 8.499998e-01 1.270000e+01 + vertex -1.409998e+02 1.215000e+01 1.270000e+01 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 -1.482882e-07 + outer loop + vertex -1.409998e+02 2.285000e+01 3.748780e+01 + vertex 8.900000e+01 2.285000e+01 3.748780e+01 + vertex -1.409998e+02 2.285000e+01 4.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 -1.482882e-07 + outer loop + vertex -1.409998e+02 2.285000e+01 4.000000e+01 + vertex 8.900000e+01 2.285000e+01 3.748780e+01 + vertex 8.900000e+01 2.285000e+01 4.000000e+01 + endloop + endfacet + facet normal -0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + vertex 8.900000e+01 -9.000000e+00 3.652193e+01 + vertex -1.409998e+02 -9.000000e+00 5.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 5.000000e+00 + vertex 8.900000e+01 -9.000000e+00 3.652193e+01 + vertex 8.900000e+01 -9.000000e+00 5.000000e+00 + endloop + endfacet + facet normal -1.000000e+00 -0.000000e+00 -0.000000e+00 + outer loop + vertex -1.409998e+02 -9.000000e+00 4.000000e+01 + vertex -1.409998e+02 -9.000000e+00 3.748780e+01 + vertex -1.409998e+02 -1.235000e+01 4.000000e+01 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 -1.235000e+01 4.000000e+01 + vertex -1.409998e+02 -9.000000e+00 3.748780e+01 + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 -1.235000e+01 4.000000e+01 + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + vertex -1.409998e+02 -1.235000e+01 2.000000e+00 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 -1.235000e+01 2.000000e+00 + vertex -1.409998e+02 -9.000000e+00 3.652193e+01 + vertex -1.409998e+02 -9.000000e+00 5.000000e+00 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.409998e+02 -1.235000e+01 2.000000e+00 + vertex -1.409998e+02 -9.000000e+00 5.000000e+00 + vertex -1.409998e+02 -9.000000e+00 2.000000e+00 + endloop + endfacet + facet normal -2.292075e-17 -1.000000e+00 -0.000000e+00 + outer loop + vertex 9.150000e+01 -1.235000e+01 4.000000e+01 + vertex -1.409998e+02 -1.235000e+01 4.000000e+01 + vertex 9.150000e+01 -1.235000e+01 2.000000e+00 + endloop + endfacet + facet normal -2.292075e-17 -1.000000e+00 0.000000e+00 + outer loop + vertex 9.150000e+01 -1.235000e+01 2.000000e+00 + vertex -1.409998e+02 -1.235000e+01 4.000000e+01 + vertex -1.409998e+02 -1.235000e+01 2.000000e+00 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex 9.150000e+01 2.535000e+01 4.000000e+01 + vertex 9.150000e+01 -1.235000e+01 4.000000e+01 + vertex 9.150000e+01 2.535000e+01 2.000000e+00 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex 9.150000e+01 2.535000e+01 2.000000e+00 + vertex 9.150000e+01 -1.235000e+01 4.000000e+01 + vertex 9.150000e+01 -1.235000e+01 2.000000e+00 + endloop + endfacet + facet normal -0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex -1.434998e+02 2.535000e+01 4.000000e+01 + vertex 9.150000e+01 2.535000e+01 4.000000e+01 + vertex -1.434998e+02 2.535000e+01 2.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex -1.434998e+02 2.535000e+01 2.000000e+00 + vertex 9.150000e+01 2.535000e+01 4.000000e+01 + vertex 9.150000e+01 2.535000e+01 2.000000e+00 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 -0.000000e+00 + outer loop + vertex -1.434998e+02 8.499998e-01 1.270000e+01 + vertex -1.434998e+02 8.499998e-01 6.000000e+00 + vertex -1.434998e+02 -1.235000e+01 2.000000e+00 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.434998e+02 -1.235000e+01 2.000000e+00 + vertex -1.434998e+02 8.499998e-01 6.000000e+00 + vertex -1.434998e+02 1.215000e+01 6.000000e+00 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.434998e+02 -1.235000e+01 2.000000e+00 + vertex -1.434998e+02 1.215000e+01 6.000000e+00 + vertex -1.434998e+02 2.535000e+01 2.000000e+00 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.434998e+02 2.535000e+01 2.000000e+00 + vertex -1.434998e+02 1.215000e+01 6.000000e+00 + vertex -1.434998e+02 1.215000e+01 1.270000e+01 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.434998e+02 2.535000e+01 2.000000e+00 + vertex -1.434998e+02 1.215000e+01 1.270000e+01 + vertex -1.434998e+02 2.535000e+01 4.000000e+01 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.434998e+02 2.535000e+01 4.000000e+01 + vertex -1.434998e+02 1.215000e+01 1.270000e+01 + vertex -1.434998e+02 8.499998e-01 1.270000e+01 + endloop + endfacet + facet normal -1.000000e+00 -0.000000e+00 0.000000e+00 + outer loop + vertex -1.434998e+02 2.535000e+01 4.000000e+01 + vertex -1.434998e+02 8.499998e-01 1.270000e+01 + vertex -1.434998e+02 -1.235000e+01 4.000000e+01 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.434998e+02 -1.235000e+01 4.000000e+01 + vertex -1.434998e+02 8.499998e-01 1.270000e+01 + vertex -1.434998e+02 -1.235000e+01 2.000000e+00 + endloop + endfacet + facet normal -0.000000e+00 -1.000000e+00 -0.000000e+00 + outer loop + vertex -1.410000e+02 -1.235000e+01 4.000000e+01 + vertex -1.434998e+02 -1.235000e+01 4.000000e+01 + vertex -1.410000e+02 -1.235000e+01 2.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 0.000000e+00 + outer loop + vertex -1.410000e+02 -1.235000e+01 2.000000e+00 + vertex -1.434998e+02 -1.235000e+01 4.000000e+01 + vertex -1.434998e+02 -1.235000e+01 2.000000e+00 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.410000e+02 -9.000000e+00 4.000000e+01 + vertex -1.410000e+02 -1.235000e+01 4.000000e+01 + vertex -1.410000e+02 -9.000000e+00 2.000000e+00 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -1.410000e+02 -9.000000e+00 2.000000e+00 + vertex -1.410000e+02 -1.235000e+01 4.000000e+01 + vertex -1.410000e+02 -1.235000e+01 2.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 -0.000000e+00 1.000000e+00 + outer loop + vertex -1.410000e+02 -1.235000e+01 4.000000e+01 + vertex -1.410000e+02 -9.000000e+00 4.000000e+01 + vertex -1.434998e+02 -1.235000e+01 4.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -1.434998e+02 -1.235000e+01 4.000000e+01 + vertex -1.410000e+02 -9.000000e+00 4.000000e+01 + vertex -1.434998e+02 2.535000e+01 4.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -1.434998e+02 2.535000e+01 4.000000e+01 + vertex -1.410000e+02 -9.000000e+00 4.000000e+01 + vertex -1.409998e+02 2.285000e+01 4.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -1.434998e+02 2.535000e+01 4.000000e+01 + vertex -1.409998e+02 2.285000e+01 4.000000e+01 + vertex 8.900000e+01 2.285000e+01 4.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -1.410000e+02 -9.000000e+00 4.000000e+01 + vertex -1.409998e+02 -9.000000e+00 4.000000e+01 + vertex -1.409998e+02 2.285000e+01 4.000000e+01 + endloop + endfacet + facet normal -0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -1.434998e+02 2.535000e+01 4.000000e+01 + vertex 8.900000e+01 2.285000e+01 4.000000e+01 + vertex 9.150000e+01 2.535000e+01 4.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 9.150000e+01 2.535000e+01 4.000000e+01 + vertex 8.900000e+01 2.285000e+01 4.000000e+01 + vertex 8.900000e+01 -9.000000e+00 4.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 9.150000e+01 2.535000e+01 4.000000e+01 + vertex 8.900000e+01 -9.000000e+00 4.000000e+01 + vertex 9.150000e+01 -1.235000e+01 4.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 9.150000e+01 -1.235000e+01 4.000000e+01 + vertex 8.900000e+01 -9.000000e+00 4.000000e+01 + vertex -1.409998e+02 -1.235000e+01 4.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -1.409998e+02 -1.235000e+01 4.000000e+01 + vertex 8.900000e+01 -9.000000e+00 4.000000e+01 + vertex -1.409998e+02 -9.000000e+00 4.000000e+01 + endloop + endfacet +endsolid diff --git a/exports/insert first test .stl b/exports/insert first test .stl new file mode 100644 index 0000000..dfaad6d Binary files /dev/null and b/exports/insert first test .stl differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..1c2c847 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + Custom Streamdeck + + +
+ + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..b45f804 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1901 @@ +{ + "name": "custom-streamdeck-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "custom-streamdeck-ui", + "version": "0.1.0", + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@tailwindcss/vite": "^4.1.17", + "lucide-react": "^0.555.0", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "tailwindcss": "^4.1.17" + }, + "devDependencies": { + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "typescript": "^5.9.3", + "vite": "^8.0.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.128.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz", + "integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", + "integrity": "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", + "integrity": "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz", + "integrity": "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.555.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.555.0.tgz", + "integrity": "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz", + "integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.128.0", + "@rolldown/pluginutils": "1.0.0-rc.18" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-x64": "1.0.0-rc.18", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", + "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz", + "integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.0-rc.18", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..ec31ae8 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,27 @@ +{ + "name": "custom-streamdeck-ui", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "tsc -b && vite build", + "preview": "vite preview --host 127.0.0.1" + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@tailwindcss/vite": "^4.1.17", + "lucide-react": "^0.555.0", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "tailwindcss": "^4.1.17" + }, + "devDependencies": { + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "typescript": "^5.9.3", + "vite": "^8.0.11" + } +} + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..7e84d0a --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,970 @@ +import { + closestCenter, + DndContext, + type DragEndEvent, + PointerSensor, + useDraggable, + useDroppable, + useSensor, + useSensors +} from "@dnd-kit/core"; +import { + AppWindow, + Boxes, + Cable, + Keyboard, + Layers3, + Loader2, + MoreVertical, + MousePointerClick, + Pencil, + Play, + Plus, + Power, + RefreshCw, + RotateCw, + Route, + Save, + ShieldCheck, + Trash2, + Unplug +} from "lucide-react"; +import { type CSSProperties, type ReactNode, useEffect, useMemo, useRef, useState } from "react"; +import { api } from "./api"; +import type { ActionType, AppEntry, ButtonConfig, DeckState, Folder, PluginAction, PluginField, PluginInfo } from "./types"; + +const emptyState: DeckState = { + settings: { serial_port: null, click_check: false, active_profile_id: "", active_folder_id: "" }, + profiles: [], + folders: [], + buttons: [], + apps: [], + plugins: [], + device: { connected_port: null } +}; + +const actionLabels: Record = { + noop: "No Action", + key_combo: "Key Press", + chain: "Action Chain", + app_launch: "App Launch", + folder: "Folder", + folder_rotation: "Folder Rotation", + plugin: "Plugin" +}; + +const actionIcons: Record = { + noop: , + key_combo: , + chain: , + app_launch: , + folder: , + folder_rotation: , + plugin: +}; + +export function App() { + const [state, setState] = useState(emptyState); + const [selectedButtonId, setSelectedButtonId] = useState(""); + const [pressedButtons, setPressedButtons] = useState>({}); + const [status, setStatus] = useState("Loading"); + const [wsOpen, setWsOpen] = useState(false); + const [createMenuOpen, setCreateMenuOpen] = useState(false); + const [manageMenuOpen, setManageMenuOpen] = useState(false); + const [profileDialogOpen, setProfileDialogOpen] = useState(false); + const [folderDialogOpen, setFolderDialogOpen] = useState(false); + const [renameProfileDialogOpen, setRenameProfileDialogOpen] = useState(false); + const [renameFolderDialogOpen, setRenameFolderDialogOpen] = useState(false); + const [deleteProfileDialogOpen, setDeleteProfileDialogOpen] = useState(false); + const [deleteFolderDialogOpen, setDeleteFolderDialogOpen] = useState(false); + const [manualAppDialogOpen, setManualAppDialogOpen] = useState(false); + const socketRef = useRef(null); + const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } })); + + useEffect(() => { + api.state().then(next => { + setState(next); + setSelectedButtonId(next.buttons.find(button => button.folder_id === next.settings.active_folder_id)?.id ?? ""); + setStatus("Ready"); + }).catch(error => setStatus(error.message)); + }, []); + + useEffect(() => { + const protocol = window.location.protocol === "https:" ? "wss" : "ws"; + const socket = new WebSocket(`${protocol}://${window.location.host}/ws`); + socketRef.current = socket; + socket.onopen = () => setWsOpen(true); + socket.onclose = () => setWsOpen(false); + socket.onmessage = event => { + const message = JSON.parse(event.data); + if (message.type === "state.updated") { + setState(message.payload); + setStatus("Synced"); + } + if (message.type === "button.down" || message.type === "button.up") { + const physical = Number(message.payload?.event?.button); + setPressedButtons(current => ({ ...current, [physical]: message.type === "button.down" })); + } + if (message.type === "action.failed") { + setStatus(message.payload?.error ?? "Action failed"); + } + }; + return () => { + socketRef.current = null; + socket.close(); + }; + }, []); + + const activeProfile = state.profiles.find(profile => profile.id === state.settings.active_profile_id); + const activeFolder = state.folders.find(folder => folder.id === state.settings.active_folder_id); + const activeProfileFolders = state.folders.filter(folder => folder.profile_id === activeProfile?.id); + const firstProfile = state.profiles[0]; + const canEditLayout = state.layout?.canonical_folder_id === state.settings.active_folder_id; + const currentButtons = useMemo( + () => state.buttons.filter(button => button.folder_id === state.settings.active_folder_id).sort((a, b) => a.position - b.position), + [state.buttons, state.settings.active_folder_id] + ); + const selectedButton = state.buttons.find(button => button.id === selectedButtonId) ?? currentButtons[0]; + const folderPath = useMemo(() => buildFolderPath(state.folders, activeFolder), [state.folders, activeFolder]); + const manualApps = state.apps.filter(app => app.source === "manual"); + + useEffect(() => { + if (!currentButtons.length) return; + if (!currentButtons.some(button => button.id === selectedButtonId)) { + setSelectedButtonId(currentButtons[0].id); + } + }, [currentButtons, selectedButtonId]); + + async function sync(operation: Promise, label = "Saved") { + setStatus("Saving"); + try { + const result = await operation; + setStatus(label); + if (looksLikeState(result)) { + setState(result); + } else { + const next = await api.state(); + setState(next); + } + } catch (error) { + setStatus(error instanceof Error ? error.message : "Request failed"); + } + } + + function updateSelected(payload: Partial) { + if (!selectedButton) return; + const buttonId = selectedButton.id; + setState(current => ({ + ...current, + buttons: current.buttons.map(button => ( + button.id === buttonId + ? { ...button, ...payload, action_config: payload.action_config ?? button.action_config } + : button + )) + })); + setStatus("Saving"); + api.updateButton(buttonId, payload) + .then(saved => { + setState(current => ({ + ...current, + buttons: current.buttons.map(button => button.id === saved.id ? saved : button) + })); + setStatus("Saved"); + }) + .catch(error => { + setStatus(error instanceof Error ? error.message : "Save failed"); + api.state().then(setState).catch(() => undefined); + }); + } + + function handleDragEnd(event: DragEndEvent) { + if (!canEditLayout) { + setStatus("Layout is locked here"); + return; + } + const physical = Number(event.active.data.current?.physical); + const position = Number(event.over?.data.current?.position); + if (!physical || !position) return; + setStatus("Saving mapping"); + const command = JSON.stringify({ type: "move_button", payload: { folder_id: state.settings.active_folder_id, physical_button: physical, position } }); + if (socketRef.current?.readyState === WebSocket.OPEN) { + socketRef.current.send(command); + return; + } + const socket = new WebSocket(`${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ws`); + socket.onopen = () => { + socket.send(command); + socket.close(); + }; + } + + return ( +
+
+
+
+ +
+
+

Custom Streamdeck

+
+ {state.device.connected_port ? : } + {state.device.connected_port ?? "Pico not connected"} + / + {wsOpen ? "Live" : "Offline"} +
+
+
+
+
+ + +
+
+ + +
+
+ + {createMenuOpen && ( +
+ + +
+ )} +
+
+ + {manageMenuOpen && ( +
+ + +
+ + +
+ )} +
+ + +
+ + + +
+
+ {status === "Saving" ? : } + {status} +
+
+
+ +
+
+
+
+
{activeProfile?.name ?? "No Profile"}
+
+ {folderPath.map((folder, index) => ( + + {index > 0 && /} + + + ))} +
+
+ {canEditLayout ? "Hardware layout is editable here and syncs everywhere." : "Hardware layout is mirrored from the first profile's root folder."} +
+
+
+ + +
+ {currentButtons.map(button => ( + setSelectedButtonId(button.id)} + /> + ))} +
+
+ +
+
+

Manual Launch Targets

+ +
+
+ {manualApps.map(app => ( + + ))} + {!manualApps.length && ( +
No manual launch targets yet.
+ )} +
+
+
+ + folder.profile_id === activeProfile?.id)} + apps={state.apps} + plugins={state.plugins} + clickCheck={state.settings.click_check} + canEditLayout={canEditLayout} + onChange={updateSelected} + onTest={() => selectedButton && sync(api.testAction(selectedButton.action_type, selectedButton.action_config), "Action test sent")} + /> +
+ + setProfileDialogOpen(false)} + onSubmit={name => sync(api.createProfile(name), "Profile created")} + /> + setFolderDialogOpen(false)} + onSubmit={name => { + if (!activeProfile) return; + sync(api.createFolder(activeProfile.id, state.settings.active_folder_id, name), "Folder created"); + }} + /> + setRenameProfileDialogOpen(false)} + onSubmit={name => { + if (!activeProfile) return; + sync(api.updateProfile(activeProfile.id, { name }), "Profile renamed"); + }} + /> + setRenameFolderDialogOpen(false)} + onSubmit={name => { + if (!activeFolder) return; + sync(api.updateFolder(activeFolder.id, { name }), "Folder renamed"); + }} + /> + setDeleteProfileDialogOpen(false)} + onConfirm={() => { + if (!activeProfile) return; + sync(api.deleteProfile(activeProfile.id), "Profile deleted"); + }} + /> + setDeleteFolderDialogOpen(false)} + onConfirm={() => { + if (!activeFolder) return; + sync(api.deleteFolder(activeFolder.id), "Folder deleted"); + }} + /> + setManualAppDialogOpen(false)} + onSubmit={(name, path, args) => sync(api.addManualApp(name, path, args), "Manual app added")} + /> +
+ ); +} + +function NameDialog(props: { + open: boolean; + title: string; + label: string; + defaultValue: string; + submitLabel: string; + onClose: () => void; + onSubmit: (value: string) => void; +}) { + const [value, setValue] = useState(props.defaultValue); + + useEffect(() => { + if (props.open) setValue(props.defaultValue); + }, [props.open, props.defaultValue]); + + if (!props.open) return null; + return ( +
+
event.stopPropagation()} onSubmit={event => { + event.preventDefault(); + const trimmed = value.trim(); + if (!trimmed) return; + props.onSubmit(trimmed); + props.onClose(); + }}> +

{props.title}

+ + setValue(event.target.value)} /> + +
+ + +
+
+
+ ); +} + +function ConfirmDialog(props: { + open: boolean; + title: string; + body: string; + confirmLabel: string; + destructive?: boolean; + onClose: () => void; + onConfirm: () => void; +}) { + if (!props.open) return null; + return ( +
+
event.stopPropagation()}> +

{props.title}

+

{props.body}

+
+ + +
+
+
+ ); +} + +function ManualAppDialog(props: { + open: boolean; + onClose: () => void; + onSubmit: (name: string, path: string, args?: string) => void; +}) { + const [name, setName] = useState(""); + const [path, setPath] = useState(""); + const [args, setArgs] = useState(""); + + useEffect(() => { + if (!props.open) return; + setName(""); + setPath(""); + setArgs(""); + }, [props.open]); + + if (!props.open) return null; + return ( +
+
event.stopPropagation()} onSubmit={event => { + event.preventDefault(); + const trimmedName = name.trim(); + const trimmedPath = path.trim(); + if (!trimmedName || !trimmedPath) return; + props.onSubmit(trimmedName, trimmedPath, args.trim() || undefined); + props.onClose(); + }}> +

Add Manual App

+ + setName(event.target.value)} /> + + + setPath(event.target.value)} /> + + + setArgs(event.target.value)} /> + +
+ + +
+
+
+ ); +} + +function StatPill(props: { label: string; value: number }) { + return ( +
+ {props.label} + {props.value} +
+ ); +} + +function DeckButton(props: { button: ButtonConfig; selected: boolean; pressed: boolean; clickCheck: boolean; canEditLayout: boolean; onSelect: () => void }) { + const { setNodeRef: setDropRef, isOver } = useDroppable({ + id: `position-${props.button.position}`, + data: { position: props.button.position } + }); + const { attributes, listeners, setNodeRef: setDragRef, transform, isDragging } = useDraggable({ + id: `physical-${props.button.physical_button}`, + data: { physical: props.button.physical_button }, + disabled: !props.canEditLayout + }); + const style: CSSProperties = { + transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined, + background: `linear-gradient(145deg, ${props.button.color}, #05070b)` + }; + return ( + + ); +} + +function Inspector(props: { + button?: ButtonConfig; + folders: Folder[]; + apps: AppEntry[]; + plugins: PluginInfo[]; + clickCheck: boolean; + canEditLayout: boolean; + onChange: (payload: Partial) => void; + onTest: () => void; +}) { + const button = props.button; + if (!button) { + return ; + } + const updateConfig = (patch: Record) => props.onChange({ action_config: { ...button.action_config, ...patch } }); + return ( + + ); +} + +function ActionForm(props: { + button: ButtonConfig; + folders: Folder[]; + apps: AppEntry[]; + plugins: PluginInfo[]; + updateConfig: (patch: Record) => void; + onChange: (payload: Partial) => void; +}) { + const { button } = props; + if (button.action_type === "noop") return

This button only shows live press state.

; + if (button.action_type === "key_combo") { + return props.updateConfig({ combo: value })} />; + } + if (button.action_type === "app_launch") { + return ( + + + + ); + } + if (button.action_type === "folder") { + return ( + + + + ); + } + if (button.action_type === "folder_rotation") { + return ( + + + + ); + } + if (button.action_type === "chain") { + const steps = Array.isArray(button.action_config.steps) ? button.action_config.steps : []; + return ( +
+
+ + +
+ {steps.map((step: any, index: number) => ( +
+ + {step.action_type === "app_launch" ? ( + + ) : ( + { + const next = [...steps]; + next[index] = { ...step, action_config: { combo: value } }; + props.updateConfig({ steps: next }); + }} /> + )} + { + const next = [...steps]; + next[index] = { ...step, delay_ms: Number(value) }; + props.updateConfig({ steps: next }); + }} /> +
+ ))} +
+ ); + } + return ; +} + +function PluginForm(props: { button: ButtonConfig; plugins: PluginInfo[]; updateConfig: (patch: Record) => void }) { + const plugin = props.plugins.find(item => item.id === props.button.action_config.plugin_id); + const action = plugin?.actions.find(item => item.id === props.button.action_config.action_id); + const enabledPlugins = props.plugins.filter(item => item.enabled); + return ( +
+ + + + {plugin && ( + + + + )} + {action?.fields.map(field => ( + props.updateConfig({ fields: { ...(props.button.action_config.fields ?? {}), [field.id]: value } })} + /> + ))} + {props.plugins.some(plugin => !plugin.enabled) &&

Some plugins failed to load. Reload after fixing their backend code.

} +
+ ); +} + +function PluginFieldInput(props: { field: PluginField; value: any; onChange: (value: any) => void }) { + if (props.field.type === "boolean") { + return ; + } + if (props.field.type === "key_value") { + return ; + } + if (props.field.type === "select") { + return ( + + + + ); + } + if (props.field.type === "textarea" || props.field.type === "json") { + return ( + + props.onChange(value)} + /> + + ); + } + return ( + + props.onChange(props.field.type === "number" ? Number(value) : value)} + /> + + ); +} + +function KeyValueField(props: { field: PluginField; value: any; onChange: (value: any) => void }) { + const rows = normalizeKeyValueRows(props.value); + const updateRow = (index: number, patch: Partial<{ key: string; value: string }>) => { + const next = rows.map((row, rowIndex) => rowIndex === index ? { ...row, ...patch } : row); + props.onChange(next); + }; + const removeRow = (index: number) => props.onChange(rows.filter((_, rowIndex) => rowIndex !== index)); + return ( +
+
+ {props.field.label} + +
+
+ {rows.map((row, index) => ( +
+ updateRow(index, { key: event.target.value })} /> + updateRow(index, { value: event.target.value })} /> + +
+ ))} + {!rows.length &&
No headers configured.
} +
+
+ ); +} + +function normalizeKeyValueRows(value: any): { key: string; value: string }[] { + if (Array.isArray(value)) { + return value.map(row => ({ key: String(row?.key ?? ""), value: String(row?.value ?? "") })); + } + if (value && typeof value === "object") { + return Object.entries(value).map(([key, rowValue]) => ({ key, value: String(rowValue ?? "") })); + } + return []; +} + +function inputTypeForPluginField(field: PluginField): "text" | "number" | "url" | "password" { + if (field.type === "number" || field.type === "url" || field.type === "password") return field.type; + return "text"; +} + +function DraftTextArea(props: { + value: string | number; + placeholder?: string; + onCommit: (value: string) => void; +}) { + const [draft, setDraft] = useState(String(props.value ?? "")); + + useEffect(() => { + setDraft(String(props.value ?? "")); + }, [props.value]); + + function commit() { + const current = String(props.value ?? ""); + if (draft !== current) { + props.onCommit(draft); + } + } + + return ( +