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.
A gamepad's row now says "no rumble in this browser" when the browser has
no way to rumble it: common for a DualSense, while Chrome and Edge rumble
Xbox pads. Before, it just never rumbled.
The heartbeat now reports how many rumble/light-bar reads ran in the last
minute and what they cost on average and at worst, in microseconds: the
longest an arriving input could wait behind one.
A game talks to a virtual controller as it would to a real one, and the system
keeps what it asked for. scePadVirtualDeviceGetRemoteSetting reads it back in
the layout of a DualShock 4's output report: the two motors at [3] and [4], the
light bar at [5..7]. The layout was read on the console while games rumbled and
users signed in, and feedback.c is tested against those exact buffers.
The service reads it every 16 ms for each controller; games pulse rumble for
only tens of milliseconds, which a 100 ms poll missed.
- Rumble goes to the controller's owner as {"method":"v","params":[pad,large,
small]}, once per change, and to whoever takes a controller over, so a page
never keeps buzzing on old news. The page already drove an Android phone's
vibration and a gamepad's actuators from it.
- The light bar goes into the status everyone sees. The PS4 sets player colours
at a quarter strength, so the colour is brightened to full, keeping its hue;
an unlit bar keeps the controller's own colour.
- Without the call, controllers work as before and it is logged once.
Also: the page says "Choose user on PS4", not "on TV", since not everyone plays
on a TV. The test stub takes C4F-TEST-FEEDBACK lines to set what the "game"
wants, and the test client can wait for messages the service pushes.
Credits are down to SplashDown, whose code the virtual-device layer is
ported from; everything else Control4Free uses, with its license, stays
listed in THIRD_PARTY.md. The AI models named are the ones actually used:
Claude Opus 5.5 and Astra GPT-6.
- README: what Control4Free is and why it reaches menus and sign-in, features,
a quick start, screenshots, requirements, limitations, credits.
- docs/: installation, playing, troubleshooting (every message the page and the
app can show), how it works, and the WebSocket protocol.
- CONTRIBUTING.md: building, testing, code style, releasing.
- Screenshots of the page on phones and a PC and of the app, made against the
real service built for the host, at the example address 192.168.1.20.
- Issue forms, a contact list pointing at the guides and private security
reporting, and FUNDING.yml.
- CHANGELOG.md dated. CI now publishes this version's section of it as the
release notes rather than the whole file.
- THIRD_PARTY.md: the DualShock 4 diagram the buttons are drawn after (CC BY
3.0), and Apollo Save Tool for the package settings.
- Page: the Vibration setting no longer promises to buzz when a game rumbles,
which 1.0.0 cannot do yet.
CHANGELOG.md says what 1.0.0 is. The CI workflow builds both toolchain images,
the payload and the package, runs every host test, and on a vX.Y.Z tag checks the
tag against VERSION before publishing the ELF, the package and their SHA256 sums.
`make C4F_PROBE=1` adds a poll of scePadVirtualDeviceGetRemoteSetting for every
live controller, logging the return code and only the bytes that change. That is
the one call that looks like it could carry a game's rumble and light-bar state
back to a virtual pad, and nothing is known about its buffer, so this is how to
find out on hardware. It builds to control4free-probe.elf and is never in a
release.
Three bits of polish for people who did not write this.
The payload serves /manifest.webmanifest and /icon-192.png, and the page points
at both, so a phone can keep the controller on its home screen instead of typing
an IP address again. The icon is the app's own icon, rendered by the launcher's
drawing code and committed, so the payload build needs no renderer; a test
re-renders it and fails if the two drift apart. One size only: the gradient does
not compress, and a 512 copy would add 150 KB for a splash screen that a plain
HTTP shortcut never shows. write_png now picks the best filter per row, which
also shrinks the package icon.
When the service starts it works out which address the console is actually
reachable on, from the route to the outside world without sending anything, and
puts it in the start-up notification on the TV.
Every error the page can show now says what happened and what to do about it,
rather than "Invalid axis", and the README has a troubleshooting section covering
the ones people will actually hit.
The staged spike that worked out the VDA call order is no longer part of the
build. It is kept at tag spike-final.
- src/main.c is the service only: instance lock, credentials, MBus and pad
init, then the web service. src/server.c and include/c4f_server.h are gone
with stage 8, and so are the stage-only helpers in src/vda.c: the MBus bind
and holds, the assignment and button-map probes, and the GoldHEN klog stream
path. The service reads /dev/klog, which is what works.
- VERSION holds the only version number. Both Makefiles pass it in, the
packager reads it for param.sfo and the package name, and the build and
deploy scripts read it for the file they look for.
- The payload's log is /data/control4free/control4free.log, not spike.log.
- The page drops the protocol 1 branches: the payload has always answered 2.
A console slot is a "controller" and a pad plugged into the phone or PC is a
"gamepad", so the two no longer read as the same word.
- The host tests move to tests/, with tests/run.py to run all four suites. They
build the real sources with the PS4 calls stubbed, so they need no console.
klog_test now covers the device path only, and asserts that nothing ever
connects to GoldHEN's klog server.
On the console, 0.2.1 often could not add a controller ("Cannot read
controller sign-in events"). Its log shows why: once Control4Free let go
of GoldHEN's klog stream, every new connection read nothing for 4-5
minutes. GoldHEN's klog server serves one client at a time and is slow to
notice that one has left.
The browser service now opens /dev/klog itself, only while a controller
waits for sign-in, and releases it afterwards, so GoldHEN's klog server
works the rest of the time. It never uses GoldHEN's stream; only the
diagnostic stages still fall back to it.
Version 0.2.2 (APP_VER 00.22).
- The service checks its listener every second and notices long pauses
such as rest mode. It then drops stale connections, keeps controllers
neutral, and rebuilds the listener. Confirmed on the console: Control4Free
survives rest mode.
- A lock in /data/control4free stops a second copy before it touches the
host process's credentials or the log.
- Browser mode reads klog through GoldHEN's klog server first, after a
marker check, and falls back to /dev/klog. It holds a reader only while
controllers wait for sign-in, so GoldHEN's own klog keeps working.
- Log lines carry elapsed time, the previous run is kept as
spike.log.previous, and a heartbeat is logged every minute.
- Version 0.2.1 (APP_VER 00.21).
- The first Cross copies the bundled payload to /data/payloads and adds it
to GoldHEN's AutoRun list, then starts it if PayLoader is on. Triangle
turns auto-start on or off, or updates an older copy. The app leaves its
sandbox for this through GoldHEN's SDK call and returns right after.
- The app tries 127.0.0.1 and then the console's own address, for both
Control4Free and PayLoader, and shows why neither answered.
- The launcher API no longer requires a loopback source address, since
the app's sandbox may not appear as 127.0.0.1. The custom header and
the Origin check still keep websites out.
- The screen and messages no longer assume a phone.
An OpenOrbis app that sends the bundled payload to GoldHEN's PayLoader,
shows its address and a QR code, and stops it through the loopback API.
It draws its screen in software in the controller page's style
(stb_truetype with Roboto), and its icon with the same code.
The package (title ID CFRE00001) carries OpenOrbis's stub libc.prx and
libSceFios2.prx in sce_module/, as the toolchain asks.
The README covers GoldHEN AutoRun, the launcher, the page and the build.