193 lines
11 KiB
Markdown
193 lines
11 KiB
Markdown
# 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 перезапускается. Если переключение происходит часто (мигающие окна) — возможны краткие потери мыши.
|
||
|
||
## WebOS TV App — сборка и деплой
|
||
|
||
TV-часть — React/Enact WebOS приложение + Node.js сервис, упакованные в IPK. Исходники: `~/Developer/magic4pc/webos/`.
|
||
|
||
### Сборка
|
||
|
||
```bash
|
||
cd ~/Developer/magic4pc/webos
|
||
NODE_OPTIONS=--openssl-legacy-provider npm run build
|
||
```
|
||
|
||
**Питфолл:** `--openssl-legacy-provider` обязателен — старый webpack несовместим с Node.js 25+.
|
||
|
||
### Упаковка IPK
|
||
|
||
```bash
|
||
/Users/admin/webOS_TV_SDK/CLI/bin/ares-package dist/ service/ --outdir .
|
||
```
|
||
|
||
**Питфолл:** нужны оба аргумента `dist/` и `service/` — без `service/` Node.js сервис не попадает в IPK.
|
||
**Питфолл:** `npm run package` = `ares-package -n` (unsigned) — TV отклоняет неподписанные пакеты. Использовать SDK-команду напрямую.
|
||
|
||
### Деплой (deploy.sh)
|
||
|
||
```bash
|
||
cd ~/Developer/magic4pc/webos && ./deploy.sh
|
||
```
|
||
|
||
Скрипт: build → package → scp → close → remove → install → launch.
|
||
Для install (subscribe mode) — прямой `ssh+script` с polling на `"state":"installed"`.
|
||
|
||
**Питфолл:** `ares-install` / `ares-launch` ненадёжны — не ждут завершения. Использовать `luna-send dev/install` через script-враппер.
|
||
|
||
Версия в UI отображается как `1.1.0 (YYYY-MM-DD HH:MM)` — инжектируется webpack через `process.env.BUILD_DATE`.
|
||
|
||
## Auto-launch при включении / пробуждении TV
|
||
|
||
Magic4pc запускает настроенное приложение при включении или пробуждении TV.
|
||
|
||
| Файл | Расположение | Назначение |
|
||
|------|-------------|-----------|
|
||
| `magic4pc-settings` | PERSISTENT_DIR | ID выбранного приложения |
|
||
| `magic4pc-last-app` | PERSISTENT_DIR | Последнее активное приложение |
|
||
| `magic4pc-run-state` | `/tmp/` | `running` после первого запуска |
|
||
|
||
`PERSISTENT_DIR = /media/developer/apps/usr/palm/services/me.wouterdek.magic4pc.service`
|
||
|
||
Логика:
|
||
- **Boot/wake:** `/tmp` очищается → нет `run-state` → `freshStart=true` → запускает настроенное приложение
|
||
- **Ручной запуск:** `run-state=running` уже есть → auto-launch пропускается
|
||
|
||
`init.d` скрипт на TV (`/var/lib/webosbrew/init.d/magic4pc`) удаляет `run-state` при suspend — wake снова тригерит fresh launch. Редактировать только в репо (`tv-scripts/init.d-magic4pc.sh`), не напрямую на TV.
|
||
|
||
## WebOS Back Key + системная клавиатура
|
||
|
||
Когда системная клавиатура открыта, **первый Back** поглощается OS (закрывает клавиатуру) — `keydown` и `onButtonDown` **не стреляют**. Второй Back приходит нормально.
|
||
|
||
Фикс в `MainPanel.js` (флаг `_kbWasOpen`):
|
||
|
||
```js
|
||
// Input.onActivate:
|
||
this._kbWasOpen = true;
|
||
|
||
// onButtonDown на Back:
|
||
if (this.state.wolMacActive || this._kbWasOpen) {
|
||
this._kbWasOpen = false;
|
||
return; // подавить закрытие панели
|
||
}
|
||
```
|
||
|
||
Подходы, которые не работают: `Popup.noAutoDismiss`, timeout-эвристика, polling `document.activeElement` (клавиатура — системный оверлей, фокус не переходит на INPUT).
|
||
|
||
## Деплой и sudoers
|
||
|
||
**Пароль sudo на HTPC:** `bazzite` (см. [[htpc-access]]).
|
||
|
||
> ⚠️ `/etc/sudoers.d/bazzite-systemctl` не существует — nopasswd для systemctl не настроен.
|
||
> Для удалённых команд через SSH использовать: `echo bazzite | sudo -S <cmd>`
|
||
|
||
Деплой новой версии:
|
||
|
||
```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
|
||
```
|