Skip to main content
Glama
README.md
# Wii U Remote

An Aroma plugin and MCP server for controlling a physical Wii U: TV/GamePad
screenshots, GamePad buttons and sticks, calibrated touch, homebrew launch and
upload, native logs and console shell access.

The project contains the console plugin, Python host and a build of the wiiload
receiver with interruption and shutdown fixes. A connected physical GamePad is
required for injected input. HOME, Wii Remote, motion and arbitrary text injection
are not supported.

## Build

Install Docker and [uv](https://docs.astral.sh/uv/), then run:

```sh
make -j24
make host
```

The Docker recipes pin the open-source toolchain and console dependencies.
`uv.lock` pins Python dependencies.

| Output | Purpose |
| --- | --- |
| `bin/wiiu-remote.wps` | Screenshot, input and homebrew-control plugin |
| `bin/wiiload.wps` | Compatible application-upload receiver |
| `.build/plugin/wiiu-remote.elf` | Console-plugin debug symbols |
| `.build/wiiload/wiiload.elf` | Receiver debug symbols |

## Console installation

Copy both WPS files to `sd:/wiiu/environments/aroma/plugins/` and restart Aroma.
The remote plugin requires the WUPS backend, MemoryMappingModule,
RPXLoadingModule and WUHBUtilsModule. The wiiload receiver additionally requires
NotificationModule. See [homebrew control](docs/homebrew.md).

Remove `remote-screenshot.wps` and `remote-test.wps` when replacing those older
plugins: they share screenshot ports or inject the same controller input.
Building the project does not install files on the console.

Capture and input operate independently of Aroma's debug service. For automatic
console discovery, native logs and shell access, enable Aroma Base Plugin's
**“Enables logging and shell access via TCP”** option. Native PPC shell commands
also require IOPShellModule.

## Host setup

Start one shared server:

```sh
uv run --locked wiiu-remote-mcp
```

Connect MCP clients using Streamable HTTP at **http://127.0.0.1:4408/mcp**.
For a client using `mcp_servers` TOML configuration:

```toml
[mcp_servers.wiiu]
url = "http://127.0.0.1:4408/mcp"
startup_timeout_sec = 20
tool_timeout_sec = 900
```

Clients connect to this existing service; they should not launch a separate
backend per session. Closing a client leaves the shared server running.
Simultaneous input from multiple clients is not arbitrated. Uploaded paths refer
to files on the server machine.

The host advertises discovery on UDP4445 and listens for Aroma's reverse debug
connection on TCP7965. The connection's source address identifies the console.
Allow both directions through the firewall and stop other receivers using
TCP7965. Auto discovery selects the first enabled console; it can learn a new
address on reconnect.

To select a console explicitly, set `CONSOLE_ADDRESS` to its hostname or IP:

```sh
uv run --locked wiiu-remote-mcp --console "$CONSOLE_ADDRESS"
```

`WIIU_HOST` supplies the same default. Add `--no-debug` to disable discovery,
log collection and shell access while retaining direct screenshot/input access.
`--http-bind` and `--http-port` configure the MCP listener; `--port` selects the
console control port. Run `wiiu-remote-mcp --help` for all options.

The transports have no authentication or encryption and are intended for a
local development network. HTTP binds to loopback by default. Other machines
can reach the same service through an SSH tunnel.

## Persistent host service

```sh
make deploy
```

This installs and starts `wiiu-remote.service` for the current user. Stop a
manually launched server first so it releases TCP4408 and TCP7965. Deployment
stops an existing service, copies the host sources and locked dependencies into
an ignored `deploy/` snapshot, synchronizes that environment and restarts it.
Development edits do not affect the running snapshot until the next deployment.
Console plugin installation remains separate.

The generated unit is under `$XDG_CONFIG_HOME/systemd/user/`, normally
`~/.config/systemd/user/`. Manage it with:

```sh
systemctl --user status wiiu-remote.service
systemctl --user restart wiiu-remote.service
journalctl --user -u wiiu-remote.service -f
```

Native logs use `$XDG_STATE_HOME/wiiu-remote/`, defaulting to
`~/.local/state/wiiu-remote/`. `--log-dir` changes the parent directory; each
server run has its own timestamped directory. Deployment does not replace logs.

## MCP tools

| Tool | Operation |
| --- | --- |
| `wiiu_get_state` | Title, foreground, display sizes, capabilities and last native input |
| `wiiu_screenshot` | Fresh TV, DRC or both captures as MCP images |
| `wiiu_act` | Ordered, timed buttons, sticks, touch, drags and waits |
| `wiiu_release_all` | Cancel input and remove injected state |
| `wiiu_list_homebrew` | List SD RPX/WUHB applications and metadata |
| `wiiu_start_homebrew` | Launch an SD application by name or path |
| `wiiu_load_homebrew` | Upload and launch a host-local RPX/WUHB using Python wiiload |
| `wiiu_stop_application` | Request Menu or explicitly submit native force kill |
| `wiiu_debug_status` | Discovery, reverse connection and host-log status |
| `wiiu_debug_logs` | Read retained native output using independent cursors |
| `wiiu_debug_command` | Submit one native console shell command and collect output |

### Input and screenshots

Buttons are `A`, `B`, `X`, `Y`, `LEFT`, `RIGHT`, `UP`, `DOWN`, `ZL`, `ZR`,
`L`, `R`, `PLUS`, `MINUS`, `STICK_L` and `STICK_R`. These are controller buttons,
not keyboard characters. Stick axes range from -1 to 1, with +X right and +Y down.
Touch coordinates are DRC screenshot pixels, measured from the top-left.

```json
{
  "actions": [
    {"type": "keypress", "keys": ["A"], "duration_ms": 100},
    {"type": "click", "x": 426, "y": 240, "duration_ms": 500},
    {
      "type": "drag",
      "path": [{"x": 100, "y": 120}, {"x": 700, "y": 400}],
      "duration_ms": 600
    }
  ],
  "observe_display": "drc"
}
```

`gamepad` actions combine `buttons`, `left_stick`, `right_stick` and optional
`touch`; `wait` pauses on the console. Input actions include a native neutral
release before completion. Drags maintain contact and interpolate on the
console. Durations are minimum holds after successful native input delivery,
not exact frame counts. A batch allows 256 expanded native steps, and each
action lasts at most ten seconds. See [input semantics](docs/input.md).

During injection, buttons, sticks and touch replace physical values; unrelated
motion data passes through. Five seconds without a successful native input
read cancels an injected phase. Foreground loss and control disconnection also
cancel input. A disconnected or unpolled controller is not emulated.

Screenshots are BMP on the console connection and PNG in MCP, encoded on the
host. No screenshot files are stored by the capture operation. TV/DRC captures
are sequential and require functioning graphics presentation; capture changes
rendering timing. Successful input remains successful if its following capture
fails. Do not replay an uncertain input batch automatically.

### Homebrew and recovery

Use an exact returned path when application names are ambiguous. WUHB uploads
include assets; plain RPX uploads can retain assets from a mounted bundle.
Installed-title launch, WPS upload and command-line arguments are unsupported.
Lifecycle calls serialize and distinguish submission from observed execution.
`started` requires a new lifetime and matching executable path; application
readiness still requires observation. See [homebrew semantics](docs/homebrew.md).

A graceful stop depends on cooperative application cleanup. Force kill submits
`cos kill` through Aroma's independent debug connection. Termination can leave
the title manager waiting, producing `recovery_required`. No operation
implicitly escalates to reboot or replays a failed launch.

Native shell submission confirms host transport submission only, not parsing or
completion. Output can interleave with unrelated logs. Debug access may survive
a PPC failure, but does not guarantee recovery from every hang. See
[discovery, logging and shell semantics](docs/aroma-debug.md).

## Connections and protocol

| Port | Direction and purpose |
| --- | --- |
| TCP4405 | Host to console: framed state, input and lifecycle control |
| TCP4406 | Host to console: one TV BMP per connection |
| TCP4407 | Host to console: one DRC BMP per connection |
| TCP4299 | Host to console: wiiload application upload |
| UDP4445 | Host discovery beacon |
| TCP7965 | Console to host: Aroma logs and shell |
| TCP4408 | MCP client to host: Streamable HTTP |

The combined plugin and host use protocol v3 and must be updated together.
See the [protocol specification](docs/protocol.md) for byte layouts and completion
semantics. [Network recovery](docs/network-recovery.md) describes listener
recreation, connection renewal and read retries. Input and lifecycle mutations
are never automatically replayed.

## License and attribution

GPL-3.0-or-later; see [LICENSE](LICENSE). GX2 capture and the RGB-to-sRGB lookup
table derive from Maschell's
[ScreenshotWUPS](https://github.com/wiiu-env/ScreenshotWUPS/tree/d6b2fc846ba09dae10611495122fa5e99adf3970).
The wiiload recipe pins the upstream
[wiiload plugin](https://github.com/wiiu-env/wiiload_plugin/tree/5afa3a72565601c9158f432fd59ae6acb782e21f)
and retains its licensing and attribution with the receiver patch.

The host uses the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)
and Pillow. Console dependencies are supplied by the
[wiiu-env](https://github.com/wiiu-env) open-source projects.