196 lines
6.1 KiB
Markdown
196 lines
6.1 KiB
Markdown
---
|
||
title: HTPC — magic4pc (LG Magic Remote)
|
||
created: '2026-05-28'
|
||
updated: '2026-05-29'
|
||
type: tech
|
||
namespace: family
|
||
tags: [htpc, how-to, pitfalls]
|
||
sources:
|
||
- family/how-to/htpc-magic4pc.md
|
||
- family/how-to/magic4pc-webos.md
|
||
confidence: medium
|
||
related:
|
||
- "[[htpc-bazzite-proton]]"
|
||
- "[[tech/htpc-steam-emulators]]"
|
||
- "[[tech/tv-luna-send]]"
|
||
---
|
||
|
||
# HTPC — magic4pc (LG Magic Remote)
|
||
|
||
Go-client for using the LG Magic Remote (webOS TV app) as HTPC
|
||
mouse/keyboard input. Fork: `mallexxx/magic4pc_altclient` branch
|
||
`bazzite-linux`.
|
||
|
||
LG TV: `192.168.1.75`, HTPC: `192.168.1.86`.
|
||
|
||
## Architecture
|
||
|
||
- **TV server**: webOS app on LG TV → sends UDP to fixed source
|
||
port **9106**
|
||
- **HTPC client**: `/usr/local/bin/magic4pc`, UDP listener
|
||
- **xdotool**: single persistent process via stdin pipe (mouse +
|
||
keyboard)
|
||
- **ydotool**: gamescope-specific keys (Ctrl+1, Ctrl+2) via
|
||
`/run/user/1000/.ydotool_socket`
|
||
|
||
Session detection:
|
||
- `isGamescopeSession()` — scans `/proc/*/cmdline` for `kwin_wayland`;
|
||
if absent → gamescope (Steam Big Picture mode)
|
||
- `activeDisplay()` — KDE uses Xwayland display from kwin args; gamescope
|
||
uses `:1` (game) if visible windows present, else `:0` (Steam BP)
|
||
|
||
## Button Mapping
|
||
|
||
| Button | Keycode | Gamescope | KDE Desktop |
|
||
|--------|---------|-----------|-------------|
|
||
| Red | 403 | Ctrl+1 (Steam menu) | Super |
|
||
| Green | 404 | Escape | Escape |
|
||
| Yellow | 405 | Ctrl+2 (Steam QAM) | Middle click |
|
||
| Blue | 406 | Right click | Right click |
|
||
| Back | 461 | Mouse X1 | Mouse X1 |
|
||
| D-pad | 37-40 | Arrow keys | Arrow keys |
|
||
| Ch+/Ch- | 33/34 | PageUp/Down | PageUp/Down |
|
||
| Play/Stop/Pause | 415/413/19 | XF86Audio* | XF86Audio* |
|
||
| Touch (swipe) | — | mousemove (scaled) | mousemove |
|
||
| Tap | — | Left click | Left click |
|
||
|
||
## Mouse Coordinate Mapping
|
||
|
||
TV sends coordinates in 0–1920 × 0–1080 space.
|
||
|
||
**In gamescope:** uses letterbox algorithm.
|
||
1. Find active window: (a) under cursor via `getmouselocation`, or
|
||
(b) largest bounding-box window (non-popup, highest ID)
|
||
2. Compute 16:9 letterbox viewport from window width:
|
||
`lb_h = win_w * 9/16`
|
||
3. Compute offset: `off_y = -(lb_h - win_h) / 2`
|
||
4. Map: `x11_x = tv_x * scale + off_x`; clip negatives to 0
|
||
|
||
**In KDE:** `getdisplaygeometry` → scale = display / 1920×1080.
|
||
|
||
## Key Pitfalls
|
||
|
||
| Problem | Fix |
|
||
|---------|-----|
|
||
| TV "service error" / won't reconnect | Fixed source UDP port 9106 |
|
||
| TV stuck "waiting for client" | Server keepalive timeout 10s |
|
||
| KDE XTest prompt on every click | Single persistent xdotool process |
|
||
| KDE XTest prompt after xdotool restart | `XwaylandEisNoPrompt=true` in kwinrc |
|
||
| Wrong mouse coordinates | Dynamic scale via `xdotool getdisplaygeometry` |
|
||
| Binary lost after `rpm-ostree upgrade` | Rebuild and redeploy |
|
||
|
||
## Deploy
|
||
|
||
```bash
|
||
# Build on Mac
|
||
cd ~/Developer/magic4pc_altclient
|
||
GOOS=linux GOARCH=amd64 go build -o magic4pc .
|
||
|
||
# Copy + restart on HTPC
|
||
scp magic4pc htpc:/home/bazzite/magic4pc/magic4pc
|
||
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
|
||
```
|
||
|
||
Sudoers rule (no-password systemctl):
|
||
```
|
||
bazzite ALL=(ALL) NOPASSWD: /usr/bin/systemctl
|
||
```
|
||
|
||
## WebOS TV App — Build & Deploy
|
||
|
||
The TV-side is a React/Enact WebOS app + Node.js service bundled into
|
||
an IPK. Sources: `~/Developer/magic4pc/webos/`.
|
||
|
||
### Build
|
||
|
||
```bash
|
||
cd ~/Developer/magic4pc/webos
|
||
NODE_OPTIONS=--openssl-legacy-provider npm run build
|
||
```
|
||
|
||
**Pitfall:** `--openssl-legacy-provider` is required — the old webpack
|
||
is incompatible with Node.js 25+.
|
||
|
||
### Package IPK
|
||
|
||
```bash
|
||
/Users/admin/webOS_TV_SDK/CLI/bin/ares-package dist/ service/ --outdir .
|
||
```
|
||
|
||
**Pitfall:** both `dist/` and `service/` arguments are required —
|
||
omitting `service/` excludes the Node.js service from the IPK.
|
||
**Pitfall:** `npm run package` = `ares-package -n` (unsigned) — TV
|
||
rejects unsigned packages. Use the SDK command directly.
|
||
|
||
### Deploy (deploy.sh)
|
||
|
||
```bash
|
||
cd ~/Developer/magic4pc/webos && ./deploy.sh
|
||
```
|
||
|
||
Script flow: build → package → scp → close → remove → install → launch.
|
||
Uses [[tech/tv-luna-send]] wrapper for close/remove/launch.
|
||
For install (subscribe mode) — direct `ssh+script` with polling on
|
||
`"state":"installed"`.
|
||
|
||
**Pitfall:** `ares-install` / `ares-launch` are unreliable — they don't
|
||
wait for completion. Use `luna-send dev/install` via `script` wrapper.
|
||
|
||
### Version in UI
|
||
|
||
Displayed as `1.1.0 (YYYY-MM-DD HH:MM)` — injected by webpack at
|
||
build time via `process.env.BUILD_DATE`.
|
||
|
||
## Auto-launch on Boot/Wake
|
||
|
||
Magic4pc launches a configured app when the TV boots or wakes.
|
||
|
||
| File | Location | Purpose |
|
||
|------|----------|---------|
|
||
| `magic4pc-settings` | PERSISTENT_DIR | Selected app ID |
|
||
| `magic4pc-last-app` | PERSISTENT_DIR | Last foreground app |
|
||
| `magic4pc-run-state` | `/tmp/` | `running` after first launch |
|
||
|
||
`PERSISTENT_DIR = /media/developer/apps/usr/palm/services/me.wouterdek.magic4pc.service`
|
||
|
||
Logic:
|
||
- **Boot/wake:** `/tmp` is cleared → no `run-state` → `freshStart=true`
|
||
→ launches configured app
|
||
- **Manual launch:** `run-state=running` already present → skips auto-launch
|
||
|
||
The `init.d` script on TV (`/var/lib/webosbrew/init.d/magic4pc`) removes
|
||
`run-state` on suspend so wake triggers fresh launch. Always edit in
|
||
repo (`tv-scripts/init.d-magic4pc.sh`), never directly on TV.
|
||
|
||
## WebOS Back Key + System Keyboard
|
||
|
||
When the system keyboard is open, **Back #1** is consumed by the OS
|
||
(closes keyboard) — the `keydown` DOM event and `onButtonDown` are
|
||
**not** fired. **Back #2** arrives normally.
|
||
|
||
Fix used in `MainPanel.js` (`_kbWasOpen` flag):
|
||
|
||
```js
|
||
// Input.onActivate:
|
||
this._kbWasOpen = true;
|
||
|
||
// onButtonDown on Back:
|
||
if (this.state.wolMacActive || this._kbWasOpen) {
|
||
this._kbWasOpen = false;
|
||
return; // suppress panel close
|
||
}
|
||
```
|
||
|
||
Approaches that don't work: `Popup.noAutoDismiss`, timeout heuristic,
|
||
polling `document.activeElement` (keyboard is a system overlay, focus
|
||
never moves to INPUT).
|
||
|
||
## See Also
|
||
|
||
- [[htpc-bazzite-proton]] — Proton game compatibility on this HTPC
|
||
- [[tech/htpc-steam-emulators]] — Steam, emulators, gamepad config
|
||
- [[tech/tv-luna-send]] — luna-send SSH wrapper for WebOS TV commands
|