# magic4pc — LG Magic Remote для HTPC ## Что это Go-клиент для LG Magic Remote (webOS приложение на TV). Форк: https://github.com/mallexxx/magic4pc_altclient (бранч `bazzite-linux`) Оригинал: https://github.com/netham45/magic4pc_altclient ## Архитектура - **TV сервер**: webOS приложение на LG TV (192.168.1.75:42831), шлёт UDP - **HTPC клиент**: `/usr/local/bin/magic4pc`, UDP-клиент на фиксированном source port **9106** - **xdotool**: единый persistent процесс через stdin pipe — мышь и клавиши - **ydotool**: для gamescope-специфичных команд (Ctrl+1, Ctrl+2) через `/run/user/1000/.ydotool_socket` ## Ключевые фиксы (история) | Проблема | Решение | |----------|---------| | TV "service error" / не реконнектится | Фиксированный source UDP port 9106 (`ListenUDP(":9106")`) | | TV зависает в "waiting for client" | Server keepalive timeout 10s → auto-reconnect | | KDE XTest диалог при каждом клике | Единый xdotool процесс (не новый на каждую кнопку) | | KDE XTest диалог при рестарте xdotool | `XwaylandEisNoPrompt=true` в kwinrc | | Кривые координаты мыши | Динамический scale через `xdotool getdisplaygeometry` | | Крестик слал Ctrl/a/s/d | TV keycodes 37-40 → `Left/Up/Right/Down` keysym | | Цифры слали мусор | ASCII keycodes → `string(rune(key))` | | Мышь не двигалась на gamescope алерте | Letterbox-алгоритм: `xdotool search --onlyvisible` → 16:9 viewport → scale+offset с clamp нуля | ## Маппинг кнопок | Кнопка | Keycode | Gaming Mode (gamescope) | KDE Desktop | |--------|---------|------------------------|-------------| | Красная | 403 | Ctrl+1 (Steam меню) | Super (Win) | | Зелёная | 404 | Escape | Escape | | Жёлтая | 405 | Ctrl+2 (Steam QAM) | Средняя кнопка мыши | | Синяя | 406 | Правый клик | Правый клик | | Back | 461 | Mouse X1 (back) | Mouse X1 (back) | | Enter/OK | 13 | Return | Return | | Ch Up | 33 | PageUp | PageUp | | Ch Down | 34 | PageDown | PageDown | | D-pad Left | 37 | Left | Left | | D-pad Up | 38 | Up | Up | | D-pad Right | 39 | Right | Right | | D-pad Down | 40 | Down | Down | | Play | 415 | XF86AudioPlay | XF86AudioPlay | | Stop | 413 | XF86AudioStop | XF86AudioStop | | Pause | 19 | XF86AudioPause | XF86AudioPause | | Цифры 0-9 | 48-57 | 0-9 | 0-9 | | GUIDE | 458 | Правый клик | Правый клик | | Мышь (тач) | — | mousemove (scaled) | mousemove (scaled) | | Тап | mousedown/up | Left click | Left click | | Скролл | wheel | scroll 4/5 | scroll 4/5 | ## Определение сессии `isGamescopeSession()` — читает `/proc/*/cmdline`, ищет `kwin_wayland`. Если нет → gamescope. ## Координаты мыши TV шлёт координаты в пространстве 0–1920 × 0–1080. ### В gamescope (Steam Big Picture) При открытии окна (алерт, диалог) клиент определяет его размер через `xdotool search --onlyvisible`, так как `getactivewindow` не работает в gamescope (нет `_NET_ACTIVE_WINDOW`). Если окно единственное видимое — используется **letterbox-алгоритм**: 1. Активное окно определяется двумя стратегиями: 1а) **Под курсором** — `xdotool getmouselocation` показывает window ID под мышью 1б) **По bounding box** — собираются все visible окна, отсекаются те что внутри другого (popup меню), из оставшихся берётся с наибольшим ID (последнее открытое) 2. Вычисляется **letterbox viewport** в пропорции 16:9 на основе ширины окна: `lb_h = win_w * 9/16` 3. Вычисляется **отступ**: `off_y = -(lb_h - win_h) / 2` (отрицательный — сдвигает viewport вверх, чтобы TV y=0 совпадал с верхом чёрной полосы) 4. TV-координаты маппятся в X11 через scale и offset: `x11_x = tv_x * scale_x + off_x` `x11_y = tv_y * scale_y + off_y` где `scale_x = scale_y = lb_w / 1920` 5. Отрицательные координаты обрезаются до 0 (xdotool их не принимает) 6. Если окно не найдено — фолбэк на `getdisplaygeometry` (весь дисплей) **Пример для алерта 288×89:** - Letterbox viewport: 288×162 - Scale: 0.15×0.15 - Offset: 0, -36.5 - TV (0,0) → X11 (0,0); TV (288,89) → X11 (43, 13) ### В KDE десктопе Используется `getdisplaygeometry` — scale = display_size / 1920×1080. Офсет 0,0. ### Определение сессии - `isGamescopeSession()` — ищет `kwin_wayland` по argv[0] в /proc. Нет → gamescope - `activeDisplay()` — находит X display для инъекции: - KDE: берёт Xwayland display из аргументов kwin_wayland - Gamescope: использует `:1` (game Xwayland) если есть visible окна, иначе `:0` (Steam BP) ## Проблемы которые могут вернуться - **xdotool умирает** (write error) → KDE показывает XTest диалог. Разрешить и включить `XwaylandEisNoPrompt=true`. - **TV "service error"** — если висит намертво, cold reboot TV (выдернуть из розетки). - **Бинарь слетает** после `rpm-ostree upgrade` → пересобрать и скопировать. - **Display switching** — при смене между `:0` и `:1` xdotool перезапускается. Если переключение происходит часто (мигающие окна) — возможны краткие потери мыши. ## Деплой и sudoers На HTPC настроен sudo без пароля для systemctl: ```bash echo 'bazzite ALL=(ALL) NOPASSWD: /usr/bin/systemctl' | sudo tee /etc/sudoers.d/bazzite-systemctl ``` Деплой новой версии: ```bash # сборка на mac cd ~/Developer/magic4pc_altclient GOOS=linux GOARCH=amd64 go build -o magic4pc . # копирование на HTPC scp magic4pc htpc:/home/bazzite/magic4pc/magic4pc # на HTPC — остановить старый, скопировать, запустить echo bazzite | sudo -S systemctl stop magic4pc.service echo bazzite | sudo -S cp /home/bazzite/magic4pc/magic4pc /usr/local/bin/magic4pc echo bazzite | sudo -S systemctl start magic4pc.service # проверить systemctl is-active magic4pc.service journalctl -u magic4pc.service -n 5 ```