mirror of
https://github.com/MoHadiShibli/Control4Free.git
synced 2026-10-06 18:00:33 +02:00
When the PS4 reports a user signed in on a controller, the service asks for the user's name and adds it to the status as `user`. The PS4 may not have finished signing the user in by then, so a failed look-up is retried every half second for five seconds. The tile shows the name in place of "Controller N" (the ring keeps the number), and the controller screen reads "Controller 1 · Alex". Names are kept out of the log, since people post logs in bug reports, and they go out cleaned: quotes and backslashes escaped, and control characters or anything that isn't valid UTF-8 replaced, because a browser drops the whole WebSocket on a text frame that isn't valid UTF-8. An Invite panel, opened from a button next to the settings or from the menu, shows the page's address as a QR code for friends to scan. The console makes the code with the QR encoder the app already ships, from the address the page reached it on, and sends the module grid; the page draws it as SVG, so it still needs no libraries. The gamepad picker's tooltip had the same "Controller 1 · Controller 1" doubling the controller screen had; it now names the user too.
157 lines
7.2 KiB
Markdown
157 lines
7.2 KiB
Markdown
# Protocol
|
||
|
||
The service listens on TCP port 4264 and speaks plain HTTP/1.1 and WebSocket (RFC 6455). This page is for
|
||
anyone writing their own client or checking what the page does.
|
||
|
||
## HTTP
|
||
|
||
| Request | Answer |
|
||
|---|---|
|
||
| `GET /`, `GET /index.html`, `GET /?…` | The controller page, gzip-compressed, with a Content-Security-Policy. |
|
||
| `GET /manifest.webmanifest` | The web app manifest, for a home-screen shortcut. |
|
||
| `GET /icon-192.png` | The app icon. |
|
||
| `GET /ws` with `Upgrade: websocket` | The control socket, below. |
|
||
| `GET /api/status`, `POST /api/stop` | The launcher API, below. |
|
||
| Anything else | `404`. |
|
||
|
||
Every request needs a `Host` header that is a literal IPv4 address or `localhost`, with or without a port;
|
||
anything else gets `403`. This keeps out domain names that resolve to the console (DNS rebinding).
|
||
|
||
## The control socket
|
||
|
||
`GET /ws` upgrades to a WebSocket. The handshake is refused with `403` when it carries an `Origin` other than
|
||
`http://<the Host header>` or `null`; no `Origin` at all is accepted. Messages are UTF-8 JSON text frames of at
|
||
most 4096 bytes; a longer frame closes the connection. Fragmented messages and ping/pong control frames are
|
||
handled.
|
||
|
||
### Requests
|
||
|
||
```json
|
||
{"id": 7, "method": "claim", "params": [0]}
|
||
```
|
||
|
||
- `method`: a string.
|
||
- `params`: an array of up to 16 integers; required, even if empty.
|
||
- `id`: optional, an integer from 0 to 2^53−1. Requests with an `id` get a reply carrying the same `id`.
|
||
- `jsonrpc: "2.0"` is allowed and ignored. Any other key, or anything that isn't this shape, is answered with
|
||
an `Invalid request` error.
|
||
|
||
Controllers are numbered 0 to 3 in the protocol and 1 to 4 on screen.
|
||
|
||
| Method | Params | Reply |
|
||
|---|---|---|
|
||
| `info` | `[]` | `{"version": "1.1.0", "protocol": 2, "pads": 4}` |
|
||
| `status` | `[]` | A status object, below. |
|
||
| `claim` | the controllers this connection wants, for example `[0]` or `[0, 2]` | A status object, once every controller in the claim exists. |
|
||
| `u` | input, below | None. |
|
||
| `leave` | `[pad]` | A status object. |
|
||
| `stop` | `[]` | None; the service stops and the connection closes. |
|
||
| `ping` | `[]` | `{}` |
|
||
| `invite` | `[a, b, c, d, port]`, the address the page reached, or `[]` | `{"text": "http://192.168.1.20:4264/", "size": 29, "rows": […]}` |
|
||
|
||
**`claim`** is the whole set this connection wants: controllers missing from it that it owned are removed,
|
||
and free ones in it are created. A claim that needs a new controller is answered when the controller exists,
|
||
usually within a second; the service keeps serving every other connection in the meantime. It's refused,
|
||
changing nothing, if any controller in it belongs to another connection (`409`), if another controller is
|
||
being created at that moment (`409`), or if controllers can't be created (`503`).
|
||
|
||
**`u`** sends the complete state of one controller this connection owns. Input for any other controller is
|
||
ignored: input never claims a controller.
|
||
|
||
```
|
||
[pad, buttons, lx, ly, rx, ry, l2, r2, fingers, (id, x, y) × fingers]
|
||
```
|
||
|
||
| Field | Range |
|
||
|---|---|
|
||
| `buttons` | a bit mask, below |
|
||
| `lx`, `ly`, `rx`, `ry` | 0 to 255, 128 is centred; up and left are 0 |
|
||
| `l2`, `r2` | 0 to 255 |
|
||
| `fingers` | 0 to 2 touches on the touchpad |
|
||
| touch `id` | 0 to 127, stable while the finger stays down |
|
||
| touch `x`, `y` | 0 to 1919, 0 to 941 |
|
||
|
||
| Button | Bit | Button | Bit |
|
||
|---|---|---|---|
|
||
| Share | `0x0001` | L2 | `0x0100` |
|
||
| L3 | `0x0002` | R2 | `0x0200` |
|
||
| R3 | `0x0004` | L1 | `0x0400` |
|
||
| Options | `0x0008` | R1 | `0x0800` |
|
||
| D-pad up | `0x0010` | Triangle | `0x1000` |
|
||
| D-pad right | `0x0020` | Circle | `0x2000` |
|
||
| D-pad down | `0x0040` | Cross | `0x4000` |
|
||
| D-pad left | `0x0080` | Square | `0x8000` |
|
||
| PS | `0x10000` | Touchpad click | `0x100000` |
|
||
|
||
Send the state whenever it changes, and at least once a second while the controller is in use. After 3
|
||
seconds without input the controller goes neutral and shows as `paused`; after 15 it's removed. When a
|
||
connection closes, its controllers' buttons are released at once, and they're removed 15 seconds later unless
|
||
a connection claims them again; the page does so when it reconnects.
|
||
|
||
**`stop`** is refused with `409` while another connection owns a controller.
|
||
|
||
**`invite`** returns a QR code of the page's address: `[]` uses the console's own address. `rows` holds one
|
||
hex string per row of modules, most significant bit first, `size` modules wide; add a light border of 4
|
||
modules around it when you draw it.
|
||
|
||
### The status object
|
||
|
||
```json
|
||
{
|
||
"version": "1.1.0",
|
||
"protocol": 2,
|
||
"pads": [
|
||
{"pad": 0, "name": "Controller 1", "user": "Alex", "enabled": true, "open": true, "connected": true, "clients": 1,
|
||
"mine": true, "state": "ready", "uid": "1a2b3c4d", "color": [32, 96, 255], "reports": 5120, "error": 0}
|
||
]
|
||
}
|
||
```
|
||
|
||
| Field | Meaning |
|
||
|---|---|
|
||
| `user` | The name of the PS4 user signed in on it, or `""` (not signed in, or not known yet). |
|
||
| `open` | The virtual controller exists. |
|
||
| `connected` | A connection owns it and is sending input. |
|
||
| `clients` | 1 if a connection owns it, else 0. |
|
||
| `mine` | This connection owns it. |
|
||
| `state` | `free`, `connecting`, `select` (waiting for a user to be chosen on the PS4), `ready` (signed in), `paused` (no input lately, or nobody connected), or `error` (the console refused input). |
|
||
| `uid` | The signed-in user's ID in hex, or `unassigned-…` before sign-in. |
|
||
| `color` | The light bar's colour, brightened so its strongest channel is 255; the controller's own colour until one is set. |
|
||
| `reports` | Samples given to the console so far. |
|
||
| `error` | The console's error code for the last refused sample, or 0. |
|
||
|
||
### Messages from the service
|
||
|
||
- `{"method": "s", "params": <status>}`: the status, whenever something changes and at least once a second.
|
||
- `{"method": "v", "params": [pad, large, small]}`: rumble, 0 to 255 for each motor, sent to the controller's
|
||
owner when it changes and when a connection takes the controller over. `[pad, 0, 0]` stops it.
|
||
- `{"method": "error", "params": {"message": "…"}}`: a problem that isn't the reply to a request: a controller
|
||
removed for sitting unused, a request that couldn't be parsed, or an error for a request sent without an
|
||
`id`.
|
||
|
||
Errors in reply to a request:
|
||
|
||
```json
|
||
{"id": 7, "error": {"code": 409, "message": "Controller is in use on another device"}}
|
||
```
|
||
|
||
| Code | Meaning |
|
||
|---|---|
|
||
| `400` | The request or its values are invalid. |
|
||
| `404` | Unknown method: the page is newer than the service. |
|
||
| `409` | Someone else owns it, another controller is being created, or it isn't yours to `leave`. |
|
||
| `503` | Controllers can't be created now, the kernel log is busy, or the service is stopping. |
|
||
|
||
## The launcher API
|
||
|
||
For the app on the PS4. Both requests need the header `X-Control4Free-Launcher: 1`, and are refused with `403`
|
||
if they carry an `Origin`, an `Upgrade`, a body or `Transfer-Encoding`. A browser only sends a custom header
|
||
cross-origin after a CORS preflight, which the service never answers, so websites can't use these.
|
||
|
||
```
|
||
GET /api/status → {"application": "Control4Free", "api": 1, "version": "1.1.0", "controllers": 2, "stopping": false}
|
||
POST /api/stop → {"application": "Control4Free", "stopping": true}
|
||
```
|
||
|
||
`/api/stop` removes every controller, even ones in use, and the service exits a quarter of a second later.
|