diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4487e0c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,59 @@ +name: build + +# Builds both toolchain images, the payload, the installable package, and runs +# every host test. On a vX.Y.Z tag it also publishes the release. +on: + push: + branches: [main, next] + tags: ['v*'] + pull_request: + workflow_dispatch: + +permissions: + contents: write # the release step needs it; nothing else writes + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build the toolchain images + run: | + docker build -t control4free-build -f docker/Dockerfile docker + docker build -t control4free-launcher-build -f docker/Dockerfile.launcher docker + + - name: Build the payload and the package + run: | + docker run --rm --network none -v "$PWD:/src" -w /src \ + control4free-launcher-build bash -lc 'make && make -C launcher' + + - name: Host tests + run: | + docker run --rm --network none -v "$PWD:/src" -w /src \ + control4free-launcher-build python3 -B tests/run.py + + - name: Collect what was built + run: | + version=$(cat VERSION) + mkdir -p artifacts + cp build/control4free.elf "build/Control4Free-$version.pkg" artifacts/ + cd artifacts && sha256sum * > SHA256SUMS && cat SHA256SUMS + + - uses: actions/upload-artifact@v4 + with: + name: control4free + path: artifacts/ + if-no-files-found: error + + - name: Check the tag matches VERSION + if: startsWith(github.ref, 'refs/tags/v') + run: | + test "v$(cat VERSION)" = "${GITHUB_REF#refs/tags/}" \ + || { echo "tag ${GITHUB_REF#refs/tags/} does not match VERSION $(cat VERSION)"; exit 1; } + + - name: Publish the release + if: startsWith(github.ref, 'refs/tags/v') + env: + GH_TOKEN: ${{ github.token }} + run: gh release create "${GITHUB_REF#refs/tags/}" artifacts/* --verify-tag --notes-file CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..00cdff3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +Dates are the day the work landed. Versions follow [Semantic Versioning](https://semver.org): +PATCH for fixes, MINOR for features that break nothing, MAJOR for anything people +rely on that changes. + +## 1.0.0 — unreleased + +First release. + +Control4Free is a GoldHEN payload that turns a phone or PC into a PS4 controller +that works everywhere: the home screen, the sign-in screen and games. Up to four +of them, through the PS4's own virtual-device API, with the PS4's own +"Who's using this controller?" screen deciding who each one is. + +- A controller page the payload serves itself on port 4264. Touch controls shaped + like a DualShock 4, keyboard keys, or any gamepad the browser exposes. A layout + editor, key remapping and a per-controller picker, saved per device. +- An installable app for the PS4's home screen that starts and stops Control4Free, + shows its address and a QR code, and sets up GoldHEN's AutoRun so the console + starts it by itself after every restart. +- The page can be kept on a phone's home screen. +- Input is reported the moment it arrives, at a DualShock 4's own 4 ms cadence + while a player is doing something, and at a slow keepalive while nobody is. +- Connecting a controller never stops the other players being served. +- Recovers by itself after rest mode or a network failure: controllers have to be + picked again, which is what rest mode does to real ones too. +- Open on the local network by design, with no pairing. What that does and does + not protect is written down in [SECURITY.md](SECURITY.md). + +Tested on firmware 10.01 with GoldHEN 2.4b18.10. Rumble, light bar and motion are +not implemented. diff --git a/Makefile b/Makefile index fc0efb5..947995d 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ # Control4Free -- system-wide virtual controllers for the PS4 (GoldHEN payload). # # Build: -# make # build/control4free.elf +# make # build/control4free.elf +# make C4F_PROBE=1 # plus the rumble/light-bar research probe (dev only) # make clean # # A build needs the ps4-payload-sdk; docker/Dockerfile has it, and @@ -19,11 +20,20 @@ NAME := control4free ELF := build/$(NAME).elf OBJDIR := build/payload SOURCES := src/main.c src/log.c src/vda.c src/klog_line.c src/web.c src/net.c + +# Research build: logs what scePadVirtualDeviceGetRemoteSetting returns. Its ELF +# gets its own name, so it can never be mistaken for a release. +ifdef C4F_PROBE + EXTRA_CFLAGS := -DC4F_PROBE_SETTING + ELF := build/$(NAME)-probe.elf + OBJDIR := build/probe +endif + OBJECTS := $(SOURCES:src/%.c=$(OBJDIR)/%.o) $(OBJDIR)/client.o $(OBJDIR)/assets.o CFLAGS += -std=gnu11 -Wall -Wextra -Wpointer-arith -g -O2 CFLAGS += -MMD -MP -CFLAGS += -Iinclude -Ivendor/jsmn -DC4F_VERSION='"$(VERSION)"' +CFLAGS += -Iinclude -Ivendor/jsmn -DC4F_VERSION='"$(VERSION)"' $(EXTRA_CFLAGS) LDFLAGS += -lScePad -lSceUserService -ldl -lpthread .PHONY: all clean diff --git a/README.md b/README.md index 18dd8af..a9e4bc7 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,13 @@ keyboard, or an Xbox/other controller exposed by the browser's Gamepad API. Virtual controllers use the PS4's native user-selection screen, so they work on the home screen, at sign-in and in games. -**Status: browser control, native user sign-in, gameplay, the launcher, -GoldHEN AutoRun and recovery after rest mode have been confirmed on the -development console.** Four virtual slots are exposed; the console's device -limits apply. +Browser control, native user sign-in, gameplay, the launcher app, GoldHEN +AutoRun and recovery after rest mode are all confirmed on the development +console. Four controllers at a time; the console's own device limits apply. + +See [CHANGELOG.md](CHANGELOG.md) for what is in a release, and +[SECURITY.md](SECURITY.md) for what being open on your network does and does not +mean. ## Requirements @@ -173,6 +176,10 @@ readable over GoldHEN's FTP server, with the previous run kept beside it. ## Build +Everything builds in Docker, so nothing has to be installed on the machine. CI +(`.github/workflows/build.yml`) runs exactly these steps and attaches the payload, +the package and their SHA256 sums to each tagged release. + The [ps4-payload-sdk](https://github.com/ps4-payload-dev/sdk) toolchain is pinned in the Docker image: diff --git a/src/web.c b/src/web.c index 8413b0a..eaef4b4 100644 --- a/src/web.c +++ b/src/web.c @@ -225,6 +225,62 @@ static void c4fEnqueue(C4fWebPad *p, const ScePadData *data) p->queue[(p->head+p->count)%C4F_QUEUE_SIZE] = *data; p->count++; } +#ifdef C4F_PROBE_SETTING +/* Research only, built with `make C4F_PROBE=1`, never in a release. + * + * A real DualShock 4 is told to rumble and to change its light bar by the game. + * For a virtual pad that has to arrive through the same API the Remote Play path + * uses, and scePadVirtualDeviceGetRemoteSetting is the only call that looks like + * it carries anything back. Nothing is known about its buffer, so this polls it + * for every live controller and logs the return code once and then only what + * changes. Play something that rumbles, set a light bar, and read the log. + * + * The page already knows how to act on both ('v' and 'l' messages), so if the + * fields turn up here, driving them is a small step. */ +static void c4fProbeSetting(C4fWeb *app, uint64_t now) +{ + static uint64_t nextAt; + static unsigned char previous[C4F_MAX_PADS][256]; + static int32_t lastRet[C4F_MAX_PADS]; + static int seen[C4F_MAX_PADS]; + unsigned char current[256]; + + if (now < nextAt) return; + nextAt = now + 100; + for (int i = 0; i < C4F_MAX_PADS; i++) { + C4fWebPad *p = &app->pads[i]; + int32_t ret; + int first, from = -1, to = -1; + + if (!p->active) { seen[i] = 0; continue; } + if (!scePadVirtualDeviceGetRemoteSetting) { + if (!seen[i]) c4fLog("probe: scePadVirtualDeviceGetRemoteSetting is not exported\n"); + seen[i] = 1; + continue; + } + memset(current, 0, sizeof(current)); + ret = scePadVirtualDeviceGetRemoteSetting(p->device.handle, current); + first = !seen[i]; + seen[i] = 1; + if (first || ret != lastRet[i]) { + c4fLog("probe: controller %d GetRemoteSetting = 0x%08x\n", i + 1, (uint32_t)ret); + lastRet[i] = ret; + } + if (ret != 0) continue; + for (int b = 0; b < (int)sizeof(current); b++) + if (first || current[b] != previous[i][b]) { if (from < 0) from = b; to = b; } + if (from >= 0) { + char hex[3 * 256 + 1]; + int used = 0; + for (int b = from; b <= to && used + 3 < (int)sizeof(hex); b++) + used += snprintf(hex + used, sizeof(hex) - (size_t)used, "%02x ", current[b]); + c4fLog("probe: controller %d bytes %d-%d: %s\n", i + 1, from, to, hex); + memcpy(previous[i], current, sizeof(current)); + } + } +} +#endif + /* One kernel-log line. Our own mirrored output is skipped: c4fLog writes to klog * too, so a line that reacted to a line would feed itself for ever. The marker * c4fKlogMark writes carries no [c4f], which is how it gets through. */ @@ -594,6 +650,9 @@ int c4fWebRun(int klogFd) } long klogBytes = c4fReadKlog(app); c4fReportPads(app); +#ifdef C4F_PROBE_SETTING + c4fProbeSetting(app, now); +#endif c4fAdvanceAdd(app, now, klogBytes); for (int i = 0; i < C4F_MAX_PADS; i++) { C4fWebPad *p = &app->pads[i];