Files
MoHadiShibli--Control4Free/docs/protocol.md
T

16 KiB
Raw Permalink Blame History

Protocol

The service uses TCP port 4264 for plain HTTP/1.1 and UDP port 4264 for WebRTC data channels. This page is for anyone writing their own client or checking what the controller page does. Replies include protocol: 3, an internal message-compatibility integer checked by the controller page, separate from the application version and the WebRTC transport. The launcher HTTP API uses api: 1. For the native input fields and feedback values Control4Free exposes, see Virtual DS4 controls and values.

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.
POST /rtc A WebRTC SDP answer to the offer in the request body, as text/plain.
OPTIONS /rtc A saved-page preflight, 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).

Establishing a WebRTC connection

  1. Create an RTCPeerConnection with no STUN or TURN servers. Create the two channels below with negotiated: true before making the offer.
  2. Create an SDP offer and set it as the local description. The ICE-lite service supplies its host candidate in the answer, so the page does not need to wait for local candidate gathering to finish.
  3. POST the local description's SDP to http://<PS4-address>:4264/rtc with Content-Type: text/plain. This is raw SDP, not a JSON object. A Content-Length is required; Transfer-Encoding is refused. Headers and body together must fit the 8 KiB HTTP receive buffer.
  4. Set the returned text as the remote description with type answer, then wait for both channels to open.
Channel ID Options Messages
control 0 negotiated: true, ordered: true; no partial-reliability limit Requests and replies for selection, status, errors and other control methods.
input 1 negotiated: true, ordered: false, maxRetransmits: 0 Controller state i, motion m, link probes e, optional latency telemetry, and rumble v.

No DCEP channel-opening exchange is used. The service is ICE-lite and returns one IPv4 host candidate on UDP port 4264. The browser performs ICE checks, DTLS and SCTP establishment. DTLS certificate fingerprints use SHA-256; the service checks the browser's certificate against its offer. No media, STUN discovery service or TURN relay is involved. See SECURITY.md for what the unencrypted HTTP exchange means for authentication.

POST /rtc is refused with 403 when its Origin is other than http://<the Host header> or null; an absent Origin is accepted. For Origin: null, the answer includes Access-Control-Allow-Origin: null. OPTIONS /rtc accepts only Origin: null and returns 204, allowing POST, Content-Type and private-network access. Other endpoints do not inherit this CORS permission. Browser local-network permission rules can still apply to a saved page.

Malformed offers get 400; an oversized request gets 413; a full or unavailable service gets 503. At most eight WebRTC peers can exist at once. An unfinished setup expires after 20 seconds, and an established peer is closed after 10 seconds without incoming traffic. The page creates a new HTTP offer when reconnecting.

Channel messages are UTF-8 JSON strings, with an application limit of 4096 bytes. Input is unordered and not retransmitted by the data-channel sender; it is not guaranteed to arrive. Control is reliable and ordered while the connection remains usable. They share one SCTP association and its congestion limits; separate channel ordering does not remove network delay or packet loss.

Requests

{"id": 7, "method": "claim", "params": [0]}
  • method: a string.
  • params: an array of up to 32 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. <version> in the examples is a placeholder for the service's version, read from VERSION. Send methods on control unless their channel is identified below.

Method Params Reply
info [] {"version": "<version>", "protocol": 3, "pads": 4, "motion": true, "input": true, "latency": true}
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.
i sequenced input, below, on input None.
m sequenced motion, below, on input None.
e [sequence, clientTime], on input The same parameters as an e message on input.
latency [probeSequence, rttMs], on input, when info.latency is true None. Informational RTT for the PS4 tester.
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).

i sends the complete state of one controller this connection owns. Input for any other controller is ignored: input never claims a controller.

[pad, seq, buttons, lx, ly, rx, ry, l2, r2, fingers, (id, x, y) × fingers,
 historyCount, (eventSeq, eventButtons) × historyCount]
Field Range
seq a nonnegative sequence number that increases for each input message for this controller
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
historyCount 0 to 4 recent button changes
eventSeq, eventButtons the input sequence and complete button mask at that change

Use integers exactly representable in JavaScript, at most 2^53−1. Keep button history in increasing sequence order; each entry must precede the current seq. Once an input sequence has been applied, messages at or below it are ignored. On a later accepted message, history entries newer than the previous applied sequence are queued before its current state. History is not replayed before the first accepted state.

History carries button masks, not the original timestamps or analog positions. Recovery uses the current message's sticks, triggers and touches. It can repair some isolated losses but cannot guarantee every press, preserve its original timing, or reconstruct an analog sequence through a long loss burst. Retain a periodic complete-state message so a final release can be repaired even when nothing changes afterwards.

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

m sends the motion sensors of one controller this connection owns. Like i, it is ignored for any other controller.

[pad, seq, qx, qy, qz, qw, ax, ay, az, gx, gy, gz]
Field Range
seq an increasing nonnegative motion sequence, separate from i
qx, qy, qz, qw orientation as a quaternion, times 10000: −10000 to 10000
ax, ay, az acceleration in thousandths of a G: −16000 to 16000
gx, gy, gz angular velocity in thousandths of a radian per second: −35000 to 35000

The axes are the ones the PS4 reads a DualShock 4 in: x to the right, y up out of its face, z towards the player. A controller lying flat and still reads +1 G on y. The values go into every sample from then on, together with the buttons, sticks and touches of the last i. Before the first m, and whenever the controller goes neutral, it reports no motion: orientation 0, 0, 0, 1 and everything else 0. Late or duplicate motion sequences are ignored. Motion messages do not renew the controller-state input lease: continue sending i while the controller is in use. A service that understands m says "motion": true in its info reply.

Send the state whenever it changes, and at least once a second while the controller is in use; the page sends complete-state keepalives after one second without a change. After 3 seconds without controller-state 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 reconnects and tries to reclaim its retained controllers. PS4 logout or controller removal clears the selection instead. Input and motion sequence state is maintained for the current owner; a different owner starts a new sequence context.

e probes are echoed over the input channel for round-trip measurements. The client time is returned unchanged; it is not compared with a PS4 clock and does not impose an input freshness deadline. The page sends a probe every two seconds while connected and calculates RTT using its own monotonic clock.

When info.latency is true, a client may send latency: [probeSequence, rttMs] after receiving an echo. Use a nonnegative, increasing probe sequence no greater than 2^53−1 and an RTT from 0 to 60000 milliseconds. The service accepts one report matching this connection's most recently echoed probe, within six seconds of that echo; unmatched, repeated, expired or invalid reports are ignored. This optional diagnostic method does not acknowledge controller input or renew its lease. The number is supplied by the client, not independently measured or authenticated by the PS4.

While a claim is being answered, every controller in it is reserved: another claim from the same connection, or one that wants any of those controllers, gets 409, and so does leave from that connection. Send one claim at a time. Creating all four controllers can take up to about 18 seconds.

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

{
  "version": "<version>",
  "protocol": 3,
  "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

PS4 user logout removes that user's virtual controllers. Their status becomes free, with open, connected and mine false, clients: 0, and an empty user. Owners receive [pad, 0, 0] to stop rumble and a disconnection message. Pages clear the affected selections instead of automatically claiming them.

  • {"method": "s", "params": <status>} on control: the status, whenever something changes and at least once a second.
  • {"method": "v", "params": [pad, large, small]} on input: rumble, 0 to 255 for each motor, sent to the controller's owner when it changes and when a connection takes the controller over. Nonzero rumble is repeated every 200 ms. [pad, 0, 0] stops it; messages can be lost on this channel. The page stops rumble if no renewal arrives for 650 ms, including when a final stop message is lost.
  • {"method": "e", "params": [sequence, clientTime]} on input: the reply to a link probe.
  • {"method": "error", "params": {"message": "…"}}: a problem that isn't the reply to a request: a controller removed for sitting unused or PS4 user logout, a request that couldn't be parsed, or an error for a request sent without an id.

Errors in reply to a request:

{"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 these API endpoints never answer, so websites can't use them.

GET /api/status returns:

{
  "application": "Control4Free", "api": 1, "version": "<version>",
  "controllers": 1, "stopping": false, "latency": true,
  "pad0User": 439041101, "pad0Rtt": 8, "pad0Age": 120,
  "pad1User": -1, "pad1Rtt": -1, "pad1Age": -1,
  "pad2User": -1, "pad2Rtt": -1, "pad2Age": -1,
  "pad3User": -1, "pad3Rtt": -1, "pad3Age": -1
}

The flat padN fields use zero-based slots. padNUser is the numeric PS4 user ID for an active, signed-in controller with an owner and fresh input; otherwise it is -1. padNRtt is that owner's latest accepted RTT, or -1 when none is fresh. padNAge is the number of milliseconds since the service echoed the measured probe, or -1 with an unknown RTT. A page owning multiple controllers supplies one connection RTT shared by those slots.

The service expires RTT measurements six seconds after their probe and requires a new probe after controller neutralization or ownership changes. The launcher also accounts for the report's age and time since its HTTP check. It shows every matching slot for the selected PS4 user: a fresh number, measuring... without a fresh report, or no controller page when no slot matches. An unavailable service or one without these optional fields produces --; it does not assign a network RTT to a local controller.

POST /api/stop returns {"application": "Control4Free", "stopping": true}.

/api/stop removes every controller, even ones in use, and the service exits a quarter of a second later.