mirror of
https://github.com/MoHadiShibli/Control4Free.git
synced 2026-10-12 08:01:04 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a8f9e3d0e | ||
|
|
75743ef24b | ||
|
|
0b3b448ca5 | ||
|
|
ed5e828701 | ||
|
|
1242d7346e |
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
ko_fi: mohadishibli
|
||||
liberapay: MoHadiShibli
|
||||
@@ -3,10 +3,27 @@
|
||||
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.2.1 — 2026-10-11
|
||||
|
||||
HDMI-CEC navigation for the PS4 app.
|
||||
|
||||
- Use your TV remote to control the PS4 app through HDMI-CEC. Press **OK** to open **Actions**,
|
||||
use the arrows to choose, and press **OK** again. **Back** returns; Stop has a separate confirmation.
|
||||
The diagnostic app also supports remote navigation and retains the last received remote key.
|
||||
See [HDMI-CEC navigation](docs/hdmi-cec.md).
|
||||
- If the app cannot reach PayLoader, it tells you to enable **GoldHEN → Server Settings → Enable
|
||||
PayLoader Server** and retry Start. The setup message now fits on screen, and the installation
|
||||
guide explains when the server is needed.
|
||||
|
||||
Thanks to [NotDev-Kike](https://github.com/MoHadiShibli/Control4Free/issues/3#issuecomment-6103983733)
|
||||
for testing and confirming OK, arrows, Back and service startup with a Samsung TV remote on firmware 11.02.
|
||||
|
||||
## 1.2.0 — 2026-10-09
|
||||
|
||||
Gamepad mapping.
|
||||
|
||||

|
||||
|
||||
- Map a gamepad on a DualShock 4: each control shows what drives it and lights up when pressed.
|
||||
Click a control, then press the gamepad button for it. Map browser-exposed inputs from gamepads, wheels,
|
||||
guitars, pedals and arcade sticks to DualShock 4 buttons, sticks and triggers. Wheel and guitar model
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<a href="https://github.com/MoHadiShibli/Control4Free/releases/latest"><img src="https://img.shields.io/github/v/release/MoHadiShibli/Control4Free?label=release" alt="Latest release"></a>
|
||||
<a href="https://github.com/MoHadiShibli/Control4Free/actions/workflows/build.yml"><img src="https://github.com/MoHadiShibli/Control4Free/actions/workflows/build.yml/badge.svg" alt="Build"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-GPL--3.0-blue" alt="License: GPL v3"></a>
|
||||
<a href="https://ko-fi.com/mohadishibli"><img src="https://img.shields.io/badge/Ko--fi-support%20this%20project-FF5E5B?logo=ko-fi&logoColor=white" alt="Support on Ko-fi"></a>
|
||||
<a href="https://liberapay.com/MoHadiShibli/"><img src="https://img.shields.io/badge/Liberapay-support%20this%20project-F6C915?logo=liberapay&logoColor=black" alt="Support on Liberapay"></a>
|
||||
</p>
|
||||
|
||||
**Use your phone or PC as a PS4 controller: on the home screen, at sign-in, and in games.**
|
||||
@@ -48,7 +48,7 @@ sees a virtual DualShock 4, which provides:
|
||||
light-bar colour, the one the PS4 or the game sets.
|
||||
- **Fast.** Input reaches the console the moment it arrives, at a DualShock 4's own rate while you play.
|
||||
- **An app on the PS4** that shows the address and a QR code, how many controllers are connected, and starts
|
||||
or stops Control4Free.
|
||||
or stops Control4Free. Use a controller or your TV remote through [HDMI-CEC](docs/hdmi-cec.md).
|
||||
- **Starts by itself.** One button in the app sets up GoldHEN's AutoRun, so Control4Free is ready after
|
||||
every restart.
|
||||
- **On your phone's home screen**, like an app.
|
||||
@@ -60,8 +60,8 @@ sees a virtual DualShock 4, which provides:
|
||||
1. Download `Control4Free-<version>.pkg` from the [latest release](https://github.com/MoHadiShibli/Control4Free/releases/latest).
|
||||
2. Install it with GoldHEN's Package Installer: **Settings → Debug Settings → Game → Package Installer**, from a
|
||||
USB drive or `/data/pkg/`.
|
||||
3. Open **Control4Free** from **Library → Applications** and press **Cross**. It sets up auto-start and
|
||||
starts Control4Free.
|
||||
3. Enable **GoldHEN → Server Settings → Enable PayLoader Server**, then open **Control4Free** from
|
||||
**Library → Applications** and press **Cross**. It sets up auto-start and starts Control4Free.
|
||||
4. On your phone or PC, scan the QR code or open the address shown, for example `http://192.168.1.20:4264`.
|
||||
Pick a controller, then choose your user on the PS4.
|
||||
|
||||
@@ -78,6 +78,7 @@ New to GoldHEN packages? The **[installation guide](docs/installation.md)** walk
|
||||
- **[Installation](docs/installation.md)**: requirements, installing, auto-start, updating, uninstalling.
|
||||
- **[Playing](docs/playing.md)**: signing in, touch, keyboard and gamepads, the layout editor, settings.
|
||||
- **[Gamepad mapping](docs/gamepad-mapping.md)**: profiles, calibration and using several input devices together.
|
||||
- **[HDMI-CEC navigation](docs/hdmi-cec.md)**: using your TV remote in the PS4 app.
|
||||
- **[Troubleshooting](docs/troubleshooting.md)**: the messages you might see and what to do about each.
|
||||
- **[How it works](docs/how-it-works.md)**: the virtual-device API, sign-in, and what runs where.
|
||||
- **[Protocol](docs/protocol.md)**: the WebSocket protocol between the page and the console.
|
||||
@@ -87,10 +88,12 @@ New to GoldHEN packages? The **[installation guide](docs/installation.md)** walk
|
||||
|
||||
## Requirements and compatibility
|
||||
|
||||
- A PS4 with GoldHEN v2.4b18.10 or later. Tested on firmware 10.01, 13.02, 13.04 and 13.52; the
|
||||
- A PS4 with GoldHEN v2.4b18.10 or later. Tested on firmware 10.01, 11.02, 13.02, 13.04 and 13.52; the
|
||||
[compatibility table](docs/installation.md#compatibility) lists every firmware and the GoldHEN version it
|
||||
needs.
|
||||
- A phone, tablet or computer with a browser, on the same network as the PS4.
|
||||
- To start the service directly from the app, enable **GoldHEN → Server Settings → Enable PayLoader
|
||||
Server**. AutoRun can start it without this server.
|
||||
- Turn off other GoldHEN controller plugins in games you play with Control4Free. A plugin that takes over a
|
||||
signed-in user's controller can stop games from reading the virtual ones.
|
||||
- A Remote Play session (chiaki-ng, for example) works alongside Control4Free.
|
||||
@@ -121,7 +124,7 @@ Found a bug? Install the diagnostic package from the latest release first, then
|
||||
[open an issue](https://github.com/MoHadiShibli/Control4Free/issues/new/choose) with its screenshots (see
|
||||
[Reporting a problem](docs/troubleshooting.md#reporting-a-problem)). I maintain this project and read every
|
||||
report. If Control4Free is useful to you, you can help keep it going on
|
||||
[Ko-fi](https://ko-fi.com/mohadishibli).
|
||||
[Liberapay](https://liberapay.com/MoHadiShibli/).
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
+2
-1
@@ -2,7 +2,8 @@
|
||||
#
|
||||
# Holds clang/lld plus a pinned ps4-payload-sdk release, so a build needs nothing
|
||||
# installed on the host but Docker. Matches the SDK's own CI (clang-18 / lld-18).
|
||||
FROM ubuntu:24.04
|
||||
# Docker's official Ubuntu mirror avoids Docker Hub's shared pull quota.
|
||||
FROM public.ecr.aws/docker/library/ubuntu:24.04@sha256:534baea6a22c03a63003dbc8dbe78fe34bc0d7e595d9a9dc9834884ff530eb55
|
||||
|
||||
ARG SDK_VERSION=v0.9
|
||||
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
# Reuse the pinned OpenOrbis release from the existing plugin toolchain image.
|
||||
ARG OPENORBIS_IMAGE=xfangfang/goldhen_plugin@sha256:e3d2acad69a1905472bd9d73e373d1d0fb6d3ca293d9f9a6ba957b9b2f49fffc
|
||||
FROM ${OPENORBIS_IMAGE} AS openorbis
|
||||
FROM ubuntu:20.04 AS pkgcompat
|
||||
# The official release contains the same OpenOrbis files as the former pinned
|
||||
# toolchain image. Fetch it directly so this build needs no Docker Hub pulls.
|
||||
FROM control4free-build:latest AS openorbis
|
||||
ARG OPENORBIS_VERSION=v0.5.3
|
||||
ARG OPENORBIS_ARCHIVE=toolchain-llvm-18.2.zip
|
||||
ARG OPENORBIS_SHA256=5006bb8629a40093b712fac971456fa4d5ad3fafaadb16d11c10b8a93b8b496f
|
||||
RUN curl -fsSL --retry 3 -o /tmp/openorbis.zip \
|
||||
"https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/releases/download/${OPENORBIS_VERSION}/${OPENORBIS_ARCHIVE}" \
|
||||
&& echo "${OPENORBIS_SHA256} /tmp/openorbis.zip" | sha256sum -c - \
|
||||
&& mkdir /tmp/openorbis \
|
||||
&& unzip -q /tmp/openorbis.zip -d /tmp/openorbis \
|
||||
&& tar -xzf /tmp/openorbis/toolchain-llvm-18.tar.gz -C /opt \
|
||||
&& test -f /opt/OpenOrbis/PS4Toolchain/lib/crt1.o \
|
||||
&& rm -rf /tmp/openorbis /tmp/openorbis.zip
|
||||
# Docker's official Ubuntu mirror, pinned to the same image as Docker Hub.
|
||||
FROM public.ecr.aws/docker/library/ubuntu:20.04@sha256:8feb4d8ca5354def3d8fce243717141ce31e2c428701f6682bd2fafe15388214 AS pkgcompat
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends libssl1.1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
FROM control4free-build:latest
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# HDMI-CEC navigation
|
||||
|
||||
Use your TV remote to start and stop Control4Free, manage auto-start, and navigate the PS4 app.
|
||||
The normal and diagnostic packages include an **Actions** menu for remotes.
|
||||
|
||||
## Set up
|
||||
|
||||
Enable **Settings → System → Enable HDMI Device Link** on the PS4 and HDMI-CEC on the TV.
|
||||
The setting's name depends on the TV model. First check that the
|
||||
remote's arrows and OK can navigate the PS4 home screen. TV and receiver/switch compatibility
|
||||
varies; see [Sony's HDMI Device Link guide](https://manuals.playstation.net/document/en/ps4/settings/devicelink.html).
|
||||
|
||||
To start the service directly from the app, enable **GoldHEN → Server Settings → Enable
|
||||
PayLoader Server**. If the app says PayLoader did not answer, enable it and retry Start.
|
||||
The app cannot enable this GoldHEN setting automatically. AutoRun can start the service
|
||||
after a restart without the server; see [Installation](installation.md).
|
||||
|
||||
## Using the remote
|
||||
|
||||
1. Press **OK** to open **Actions**. A Menu key can also open it if exposed by the PS4.
|
||||
2. Use the arrows to choose an action, then press **OK** again. The menu includes setup/start,
|
||||
auto-start, Stop and Close when those actions are available.
|
||||
3. **Stop** opens a separate confirmation with **Cancel** selected. Choose **Stop**, release
|
||||
OK, then press it again to confirm. Stopping disconnects every virtual controller.
|
||||
4. **Back**, when the PS4 exposes it, returns from the menu or cancels confirmation. **Resume**
|
||||
also returns without needing a Back button. **Close app** leaves the service running.
|
||||
|
||||
Existing DualShock 4 shortcuts work outside the menu. Its D-pad can also open and navigate
|
||||
Actions in the normal app. Service operations finish before another operation or Close is
|
||||
available. Release held buttons after opening the app, reconnecting or returning to it.
|
||||
|
||||
## Diagnostic app
|
||||
|
||||
**Left/Right** change pages and **Up/Down** scroll while the menu is
|
||||
closed. **OK** opens Actions, which also includes **Capture kernel log**. Menu arrows move
|
||||
the selection without changing the page underneath it. The Summary page reports DS4 and
|
||||
TV-remote input status; a ready source shows its navigation button mask, and a failed source
|
||||
shows the native error code. It also retains the last active remote sample's key code and raw
|
||||
button mask, so a short press remains visible after you release it. These values describe
|
||||
the PS4's input data; they are not HDMI-CEC wire command numbers.
|
||||
|
||||
If a remote fails inside the app, press and release the affected button, then photograph
|
||||
Summary or save its report. See [Reporting a problem](troubleshooting.md#reporting-a-problem).
|
||||
|
||||
## Confirmed setup and limits
|
||||
|
||||
| TV | Remote | PS4 firmware | GoldHEN | Confirmed behavior |
|
||||
|---|---|---|---|---|
|
||||
| Samsung UN55NU7090P | AA59-00818A (TM1240) | 11.02 | v2.4b18.12 | OK, arrows, RETURN/Back and service startup with PayLoader Server enabled |
|
||||
|
||||
This setup was [confirmed by NotDev-Kike](https://github.com/MoHadiShibli/Control4Free/issues/3#issuecomment-6103983733)
|
||||
with no physical DS4 connected. Other TV/remote models, simultaneous DS4 use, HDMI input
|
||||
switching and rest/wake behavior still need hardware reports. Held-button handling, recovery
|
||||
and Stop confirmation have automated tests; their hardware behavior was not separately reported.
|
||||
|
||||
The app reads the PS4's remote-control stream independently of DS4 input. TV keys control
|
||||
the app only. Media/color keys and TV power/volume control are outside this implementation.
|
||||
|
||||
For another setup, report the TV and remote models, PS4 firmware, GoldHEN version, any
|
||||
receiver/switch between TV and console, and which buttons work. Diagnostic screenshots help
|
||||
identify inputs the PS4 exposes.
|
||||
+14
-4
@@ -14,6 +14,9 @@ Control4Free comes in two pieces:
|
||||
run ELF payloads like Control4Free reliably, and have no auto-start. See [Compatibility](#compatibility) for
|
||||
each firmware.
|
||||
- **Debug Settings** turned on in GoldHEN, for the Package Installer.
|
||||
- **PayLoader Server** enabled for starting the service directly from the app:
|
||||
**GoldHEN → Server Settings → Enable PayLoader Server**. AutoRun and Payloader LaunchPad can start the
|
||||
service without this server.
|
||||
- A phone, tablet or computer with a modern browser, on the same network as the PS4.
|
||||
|
||||
## Compatibility
|
||||
@@ -25,12 +28,13 @@ Control4Free's start-up code, but nobody has tried them yet.
|
||||
| Firmware | GoldHEN | Status |
|
||||
|---|---|---|
|
||||
| 10.01 | v2.4b18.10 or later | **Tested** (v2.4b18.10) |
|
||||
| 11.02 | v2.4b18.10 or later | **Tested** (v2.4b18.12) |
|
||||
| 13.02, 13.04 | v2.4b18.12 or later | **Tested** (v2.4b18.12) |
|
||||
| 13.52 | v2.4b18.11 or later | **Tested** (v2.4b18.12), with Control4Free 1.1.1 or later |
|
||||
| 13.50 | v2.4b18.12 or later | Not tested |
|
||||
| 13.00 | v2.4b18.10 or later | Not tested |
|
||||
| 12.00, 12.02, 12.50, 12.52 | v2.4b18.10 or later | Not tested |
|
||||
| 5.05, 6.71, 6.72, 7.0x, 7.5x, 8.0x, 8.5x, 9.0x, 9.5x, 9.60, 10.00, 10.50, 10.70, 10.71, 11.0x, 11.5x | v2.4b18.10 or later | Not tested |
|
||||
| 5.05, 6.71, 6.72, 7.0x, 7.5x, 8.0x, 8.5x, 9.0x, 9.5x, 9.60, 10.00, 10.50, 10.70, 10.71, other 11.0x, 11.5x | v2.4b18.10 or later | Not tested |
|
||||
|
||||
The older a firmware, the less certain it is that the PS4's virtual-controller API behaves as on the tested
|
||||
ones. If you try an untested firmware, an issue saying whether it worked helps others, with a
|
||||
@@ -50,13 +54,15 @@ The app appears in **Library → Applications**.
|
||||
|
||||
## First start
|
||||
|
||||
Open **Control4Free** and press **Cross**. The app:
|
||||
In GoldHEN, open **Server Settings** and check **Enable PayLoader Server**. Then open **Control4Free** and
|
||||
press **Cross**. The app:
|
||||
|
||||
1. copies the service to `/data/payloads/control4free.elf`;
|
||||
2. adds it to GoldHEN's AutoRun list, `/data/GoldHEN/payloads.ini`, so GoldHEN starts it every time it
|
||||
loads;
|
||||
3. starts it straight away if GoldHEN's **PayLoader** is on. If it isn't, turn PayLoader on in GoldHEN's
|
||||
settings and press **Cross** again. AutoRun also starts Control4Free each time GoldHEN loads.
|
||||
3. starts it straight away through GoldHEN's PayLoader Server. If the server is unavailable, auto-start
|
||||
stays enabled; enable PayLoader Server and retry, or let AutoRun start Control4Free the next time
|
||||
GoldHEN loads.
|
||||
|
||||
Once it's running, the app shows the address to open, for example `http://192.168.1.20:4264`, and a QR code
|
||||
for your phone's camera. The PS4 also shows the address in a notification when the service starts.
|
||||
@@ -78,6 +84,10 @@ screen, that's fine too: the app doesn't get in the way of Control4Free.
|
||||
| **Square**, then **Cross** | Stops Control4Free and disconnects every controller. |
|
||||
| **Circle** | Closes the app. Control4Free keeps running without it. |
|
||||
|
||||
With a TV remote over HDMI-CEC, press **OK** to open **Actions**, use the arrows to choose,
|
||||
and press **OK** again. **Back** returns. See [HDMI-CEC navigation](hdmi-cec.md) for setup
|
||||
and compatibility.
|
||||
|
||||
## Updating
|
||||
|
||||
1. Install the new package over the old one. Delete the old app first if the installer refuses.
|
||||
|
||||
@@ -129,8 +129,9 @@ has loaded, and check it has a controller under **Gamepads on this device**.
|
||||
|
||||
## In the app on the PS4
|
||||
|
||||
**"PayLoader did not answer."** Turn on GoldHEN's PayLoader and press **Cross** again, or restart the PS4 and
|
||||
run the jailbreak: with auto-start on, Control4Free starts by itself.
|
||||
**"PayLoader did not answer."** Open **GoldHEN → Server Settings → Enable PayLoader Server**, check the
|
||||
option and retry **Start** in the app, or restart the PS4 and run the jailbreak: with auto-start on,
|
||||
Control4Free starts by itself.
|
||||
|
||||
**"Control4Free is not responding."** Wait a few seconds after the PS4 wakes from rest mode, then press
|
||||
**Cross** again. If it stays stuck, restart the PS4 and run the jailbreak.
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ CC := clang-18
|
||||
CXX := clang++-18
|
||||
LD := ld.lld-18
|
||||
CFLAGS := --target=x86_64-pc-freebsd12-elf -fPIC -funwind-tables -O2 -Wall -Wextra -Werror -MMD -MP -D_DEFAULT_SOURCE -isysroot $(SDK) -isystem $(SDK)/include -I../vendor/jsmn -I../vendor/qrcodegen -I../vendor/stb -DC4F_LAUNCHER_VERSION='"$(VERSION)"' $(DIAG_FLAGS)
|
||||
OBJECTS += $(OUT)/main.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/logo.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o
|
||||
OBJECTS += $(OUT)/main.o $(OUT)/input.o $(OUT)/navigation.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/logo.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o
|
||||
LIBS := -lc -lkernel -lSceVideoOut -lScePad -lSceUserService -lSceSystemService -lSceNetCtl
|
||||
FONTS := ../vendor/roboto/Roboto-Light.ttf ../vendor/roboto/Roboto-Regular.ttf
|
||||
.PHONY: all package
|
||||
|
||||
+1
-1
@@ -184,7 +184,7 @@ int c4fAutorunCheck(char *problem, size_t size)
|
||||
state = installedSize == c4fBundledSize && !memcmp(installed, c4fBundled, installedSize) ? C4F_AUTORUN_ON : C4F_AUTORUN_OUTDATED;
|
||||
else if (errno == ENOENT) {
|
||||
state = C4F_AUTORUN_OUTDATED;
|
||||
snprintf(problem, size, "Enabled, but the payload is missing; Triangle repairs it");
|
||||
snprintf(problem, size, "Enabled, but the payload is missing; choose Update auto-start in Actions or press Triangle");
|
||||
} else {
|
||||
state = C4F_AUTORUN_UNKNOWN;
|
||||
snprintf(problem, size, "Enabled, but the payload cannot be inspected (errno %d)", errno);
|
||||
|
||||
+12
-6
@@ -371,8 +371,8 @@ static char *c4fSummary(const C4fDiagState *s)
|
||||
">> here and take screenshots of pages 1 and 2: page 2 lists every connection it saw.\n",
|
||||
s->version, s->address[0] ? s->address : "the PS4's address with :4264");
|
||||
else if (c4fStarted && c4fContains(c4fStartMessage, "PayLoader did not answer"))
|
||||
c4fAdd(&t, ">> GoldHEN's PayLoader (port 9090) did not take the payload. Turn PayLoader on in GoldHEN's\n"
|
||||
">> settings, then press Cross again.\n");
|
||||
c4fAdd(&t, ">> GoldHEN's PayLoader Server (port 9090) did not take the payload. Open GoldHEN > Server Settings,\n"
|
||||
">> check Enable PayLoader Server, then retry Start.\n");
|
||||
else if (c4fStarted && c4fStartResult == -2 && !c4fLogChanged)
|
||||
c4fAdd(&t, ">> The payload was handed to PayLoader, but Control4Free's own code never ran (it wrote no log).\n"
|
||||
">> PayLoader did not run it, or it stopped while starting up on this firmware. The kernel log\n"
|
||||
@@ -396,6 +396,7 @@ static char *c4fSummary(const C4fDiagState *s)
|
||||
c4fAdd(&t, "\nThis app\n");
|
||||
c4fAdd(&t, " version %s, bundled payload %s, open for %llu s\n", C4F_LAUNCHER_VERSION, c4fBundled,
|
||||
(unsigned long long)((c4fLauncherTimeMs() - c4fBootMs) / 1000));
|
||||
c4fAdd(&t, " input: %s\n", s->inputNote && s->inputNote[0] ? s->inputNote : "waiting");
|
||||
|
||||
c4fAdd(&t, "\nService (port 4264)\n");
|
||||
if (s->running == 1) c4fAdd(&t, " running, version %s, %d controller(s)\n", s->version, s->controllers);
|
||||
@@ -579,11 +580,12 @@ void c4fDrawDiag(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
__DATE__, __TIME__, page + 1, C4F_DIAG_PAGES, c4fPageNames[page]);
|
||||
c4fText(&c, C4F_FONT_REGULAR, C4F_D_TITLE, C4F_D_LEFT, 48, C4F_D_ACCENT, line);
|
||||
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, 84, C4F_D_FAINT,
|
||||
"L1 / R1: change page Up / Down: scroll Options: read the kernel log again "
|
||||
"Cross: start Triangle: auto-start on/off Square: stop Circle: close");
|
||||
"Left / Right: page Up / Down: scroll TV remote OK: actions "
|
||||
"Cross: start Triangle: auto-start Square: stop Circle: close");
|
||||
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, 114, C4F_D_WARN,
|
||||
"Please take a screenshot of EVERY page (hold SHARE, or press SHARE then Triangle) and send them all.");
|
||||
if (s->confirmStop) snprintf(line, sizeof(line), "Press Cross to stop Control4Free, or Circle to cancel.");
|
||||
if (s->confirmStop) snprintf(line, sizeof(line), "%s", s->confirmMenu ?
|
||||
"Select Cancel or Stop with the arrows, then press OK." : "Press Cross to stop Control4Free, or Circle to cancel.");
|
||||
else snprintf(line, sizeof(line), "%s%s", s->busy ? "WORKING: " : "Now: ", s->message);
|
||||
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, 146, C4F_D_TEXT, line);
|
||||
c4fFillRect(&c, C4F_D_LEFT, 160, (int)width, 1, C4F_D_LINE);
|
||||
@@ -591,7 +593,7 @@ void c4fDrawDiag(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
pthread_mutex_lock(&c4fLock);
|
||||
char *text = strdup(c4fPages[page] ? c4fPages[page] : "(not read yet)");
|
||||
pthread_mutex_unlock(&c4fLock);
|
||||
if (!text) return;
|
||||
if (!text) { c4fDrawActionOverlay(pixels, s); return; }
|
||||
|
||||
/* Wrap every line, then show the window the scroll position picks. */
|
||||
int count = 0, capacity = 256;
|
||||
@@ -626,8 +628,12 @@ void c4fDrawDiag(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
}
|
||||
snprintf(line, sizeof(line), "lines %d-%d of %d", count ? first + 1 : 0, first + visible < count ? first + visible : count, count);
|
||||
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, 1072, C4F_D_FAINT, line);
|
||||
const char *remoteHint = "TV remote: OK actions / Back returns L1 / R1: page Options: kernel log";
|
||||
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_SCREEN_WIDTH - C4F_D_LEFT -
|
||||
c4fTextWidth(C4F_FONT_REGULAR, C4F_D_SIZE, remoteHint), 1072, C4F_D_FAINT, remoteHint);
|
||||
free(rows);
|
||||
free(text);
|
||||
c4fDrawActionOverlay(pixels, s);
|
||||
}
|
||||
|
||||
int c4fDiagMaxScroll(int page)
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ extern "C" {
|
||||
|
||||
typedef struct {
|
||||
int running, controllers, autorun, locked, busy, confirmStop;
|
||||
const char *version, *problem, *message, *address, *autorunNote;
|
||||
const char *version, *problem, *message, *address, *autorunNote, *inputNote;
|
||||
} C4fDiagState;
|
||||
|
||||
/* Records what is known at start-up and reads the kernel log's backlog. */
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#include "input.h"
|
||||
#include <string.h>
|
||||
|
||||
uint32_t c4fInputRemoteButtons(uint32_t rawButtons, uint8_t keyCode)
|
||||
{
|
||||
/* PS5 SDL reads this Sony key table alongside ordinary pad buttons:
|
||||
* https://github.com/ps5-payload-dev/SDL/blob/ee4c47dc0d617b3bc8f35108f9956baf228a1322/src/video/ps5/SDL_ps5remote.c
|
||||
* Applying these navigation codes to PS4 is a hardware-test candidate;
|
||||
* they are not the HDMI-CEC wire command numbers. */
|
||||
if (rawButtons & C4F_INPUT_BUTTON_INTERCEPTED) return rawButtons;
|
||||
switch (keyCode) {
|
||||
case 13: return rawButtons | C4F_INPUT_BUTTON_CROSS;
|
||||
case 15: return rawButtons | C4F_INPUT_BUTTON_CIRCLE;
|
||||
case 18: return rawButtons | C4F_INPUT_BUTTON_OPTIONS;
|
||||
default: return rawButtons;
|
||||
}
|
||||
}
|
||||
|
||||
static void c4fInputRelease(C4fInput *input, C4fInputSource *source, uint64_t retryAt)
|
||||
{
|
||||
if (source->handle >= 0 && source->owned && input->ops.close)
|
||||
input->ops.close(input->ops.context, source->handle);
|
||||
source->handle = -1;
|
||||
source->owned = 0;
|
||||
source->ready = 0;
|
||||
source->user = -1;
|
||||
source->previous = 0;
|
||||
source->retryAt = retryAt;
|
||||
}
|
||||
|
||||
void c4fInputInit(C4fInput *input, const C4fInputOps *ops)
|
||||
{
|
||||
memset(input, 0, sizeof(*input));
|
||||
if (ops) input->ops = *ops;
|
||||
for (int i = 0; i < C4F_INPUT_SOURCE_COUNT; i++) {
|
||||
input->sources[i].handle = -1;
|
||||
input->sources[i].user = -1;
|
||||
}
|
||||
}
|
||||
|
||||
static void c4fInputRead(C4fInput *input, int index, uint64_t now, C4fInputSample *sample)
|
||||
{
|
||||
C4fInputSource *source = &input->sources[index];
|
||||
if (!input->ops.open || !input->ops.read) return;
|
||||
if (source->handle < 0) {
|
||||
if (now < source->retryAt) return;
|
||||
int32_t user = C4F_INPUT_SYSTEM_USER;
|
||||
if (index == C4F_INPUT_STANDARD &&
|
||||
(!input->ops.initialUser || input->ops.initialUser(input->ops.context, &user) != 0 || user < 0)) {
|
||||
source->retryAt = now + C4F_INPUT_RETRY_MS;
|
||||
return;
|
||||
}
|
||||
int owned = 0;
|
||||
int port = index == C4F_INPUT_STANDARD ? C4F_INPUT_PORT_STANDARD : C4F_INPUT_PORT_REMOTE;
|
||||
int handle = input->ops.open(input->ops.context, user, port, &owned);
|
||||
if (handle < 0) {
|
||||
source->retryAt = now + C4F_INPUT_RETRY_MS;
|
||||
return;
|
||||
}
|
||||
source->handle = handle;
|
||||
source->owned = !!owned;
|
||||
source->user = user;
|
||||
source->ready = 0;
|
||||
source->previous = 0;
|
||||
}
|
||||
uint32_t buttons = 0;
|
||||
int connected = 0;
|
||||
if (input->ops.read(input->ops.context, source->handle, &buttons, &connected) != 0 || !connected) {
|
||||
c4fInputRelease(input, source, now + C4F_INPUT_RETRY_MS);
|
||||
return;
|
||||
}
|
||||
sample->available = 1;
|
||||
if (!source->ready) {
|
||||
if (!buttons) source->ready = 1;
|
||||
return;
|
||||
}
|
||||
sample->buttons = buttons;
|
||||
sample->pressed = buttons & ~source->previous;
|
||||
source->previous = buttons;
|
||||
}
|
||||
|
||||
void c4fInputPoll(C4fInput *input, uint64_t now, C4fInputFrame *frame)
|
||||
{
|
||||
memset(frame, 0, sizeof(*frame));
|
||||
c4fInputRead(input, C4F_INPUT_STANDARD, now, &frame->standard);
|
||||
c4fInputRead(input, C4F_INPUT_REMOTE, now, &frame->remote);
|
||||
}
|
||||
|
||||
void c4fInputReset(C4fInput *input, uint64_t now)
|
||||
{
|
||||
for (int i = 0; i < C4F_INPUT_SOURCE_COUNT; i++)
|
||||
c4fInputRelease(input, &input->sources[i], now);
|
||||
}
|
||||
|
||||
void c4fInputClose(C4fInput *input)
|
||||
{
|
||||
c4fInputReset(input, 0);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#ifndef C4F_LAUNCHER_INPUT_H
|
||||
#define C4F_LAUNCHER_INPUT_H
|
||||
#include <stdint.h>
|
||||
|
||||
/* These sources use the native pad API independently. Remote-control input
|
||||
* belongs to the system user, rather than the launcher's signed-in user. */
|
||||
enum { C4F_INPUT_STANDARD, C4F_INPUT_REMOTE, C4F_INPUT_SOURCE_COUNT };
|
||||
enum { C4F_INPUT_PORT_STANDARD = 0, C4F_INPUT_PORT_REMOTE = 16 };
|
||||
#define C4F_INPUT_SYSTEM_USER 0xff
|
||||
#define C4F_INPUT_RETRY_MS 1000
|
||||
|
||||
/* Native read-side buttons used by launcher navigation. */
|
||||
#define C4F_INPUT_BUTTON_OPTIONS UINT32_C(0x000008)
|
||||
#define C4F_INPUT_BUTTON_UP UINT32_C(0x000010)
|
||||
#define C4F_INPUT_BUTTON_RIGHT UINT32_C(0x000020)
|
||||
#define C4F_INPUT_BUTTON_DOWN UINT32_C(0x000040)
|
||||
#define C4F_INPUT_BUTTON_LEFT UINT32_C(0x000080)
|
||||
#define C4F_INPUT_BUTTON_L1 UINT32_C(0x000400)
|
||||
#define C4F_INPUT_BUTTON_R1 UINT32_C(0x000800)
|
||||
#define C4F_INPUT_BUTTON_TRIANGLE UINT32_C(0x001000)
|
||||
#define C4F_INPUT_BUTTON_CIRCLE UINT32_C(0x002000)
|
||||
#define C4F_INPUT_BUTTON_CROSS UINT32_C(0x004000)
|
||||
#define C4F_INPUT_BUTTON_SQUARE UINT32_C(0x008000)
|
||||
/* PS4 reverse definitions identify bit31 as system-intercepted input. This is
|
||||
* a read-side flag, not an additional virtual-controller button. */
|
||||
#define C4F_INPUT_BUTTON_INTERCEPTED UINT32_C(0x80000000)
|
||||
|
||||
typedef struct {
|
||||
void *context;
|
||||
int (*initialUser)(void *context, int32_t *user);
|
||||
/* Returns a handle, or a negative error. owned is true only for handles
|
||||
* opened here; an already-open handle may be borrowed and must not close. */
|
||||
int (*open)(void *context, int32_t user, int port, int *owned);
|
||||
int (*read)(void *context, int handle, uint32_t *buttons, int *connected);
|
||||
void (*close)(void *context, int handle);
|
||||
} C4fInputOps;
|
||||
|
||||
typedef struct {
|
||||
int handle, owned, ready;
|
||||
int32_t user;
|
||||
uint32_t previous;
|
||||
uint64_t retryAt;
|
||||
} C4fInputSource;
|
||||
|
||||
typedef struct {
|
||||
C4fInputOps ops;
|
||||
C4fInputSource sources[C4F_INPUT_SOURCE_COUNT];
|
||||
} C4fInput;
|
||||
|
||||
typedef struct {
|
||||
uint32_t buttons, pressed;
|
||||
int available;
|
||||
} C4fInputSample;
|
||||
|
||||
typedef struct {
|
||||
C4fInputSample standard, remote;
|
||||
} C4fInputFrame;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
void c4fInputInit(C4fInput *input, const C4fInputOps *ops);
|
||||
/* Adds navigation keys reported separately by the system remote-control
|
||||
* stream, preserving the ordinary button mask and unknown key codes. */
|
||||
uint32_t c4fInputRemoteButtons(uint32_t rawButtons, uint8_t keyCode);
|
||||
/* Each recovered source requires a neutral sample before generating edges.
|
||||
* Failure or disconnect produces neutral input and a bounded retry. */
|
||||
void c4fInputPoll(C4fInput *input, uint64_t now, C4fInputFrame *frame);
|
||||
/* Focus changes may reset sources; durable launcher state is unaffected. */
|
||||
void c4fInputReset(C4fInput *input, uint64_t now);
|
||||
void c4fInputClose(C4fInput *input);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
+160
-36
@@ -1,6 +1,7 @@
|
||||
/* Native PS4 launcher. The controller service runs outside this application. */
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -13,6 +14,8 @@
|
||||
#include <orbis/UserService.h>
|
||||
#include <orbis/VideoOut.h>
|
||||
#include "autorun.h"
|
||||
#include "input.h"
|
||||
#include "navigation.h"
|
||||
#include "sandbox.h"
|
||||
#include "screen.h"
|
||||
#include "service.h"
|
||||
@@ -40,7 +43,7 @@ static int c4fSetUp(char *message, size_t size)
|
||||
result = c4fLauncherStart(payload, payloadSize, started, sizeof(started));
|
||||
if (result == 0) snprintf(message, size, "Set up and running. GoldHEN will also start it after each restart.");
|
||||
else if (result == -2) snprintf(message, size, "%s", started);
|
||||
else snprintf(message, size, "Auto-start is on: Control4Free starts the next time GoldHEN loads. %s", started);
|
||||
else snprintf(message, size, "Auto-start is on. %s", started);
|
||||
return result == -2 ? -2 : 0;
|
||||
}
|
||||
|
||||
@@ -111,7 +114,7 @@ static void *c4fWorker(void *)
|
||||
C4fLauncherScreen copy = c4fScreen;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
C4fDiagState diag = { running, status.controllers, autorun, copy.locked, copy.busy, copy.confirmStop,
|
||||
status.version, problem, copy.message, copy.address, copy.autorunNote };
|
||||
status.version, problem, copy.message, copy.address, copy.autorunNote, copy.inputNote };
|
||||
unsigned generation = c4fDiagRefresh(&diag);
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
c4fScreen.diagGeneration = generation;
|
||||
@@ -166,6 +169,115 @@ static void c4fCloseVideo(void)
|
||||
if (c4fVideoSize) sceKernelReleaseDirectMemory(c4fVideoOffset, c4fVideoSize);
|
||||
}
|
||||
|
||||
/* The pinned SDK omits this error from Pad.h. A handle already opened by the
|
||||
* application can be borrowed, but must not be closed by our reader. */
|
||||
#define C4F_PAD_ERROR_ALREADY_OPENED 0x80920004u
|
||||
static_assert(C4F_INPUT_SYSTEM_USER == ORBIS_USER_SERVICE_USER_ID_SYSTEM, "system input user");
|
||||
static_assert(C4F_INPUT_PORT_STANDARD == ORBIS_PAD_PORT_TYPE_STANDARD, "standard input port");
|
||||
static_assert(C4F_INPUT_BUTTON_UP == ORBIS_PAD_BUTTON_UP, "Up button");
|
||||
static_assert(C4F_INPUT_BUTTON_RIGHT == ORBIS_PAD_BUTTON_RIGHT, "Right button");
|
||||
static_assert(C4F_INPUT_BUTTON_DOWN == ORBIS_PAD_BUTTON_DOWN, "Down button");
|
||||
static_assert(C4F_INPUT_BUTTON_LEFT == ORBIS_PAD_BUTTON_LEFT, "Left button");
|
||||
static_assert(C4F_INPUT_BUTTON_CROSS == ORBIS_PAD_BUTTON_CROSS, "OK button");
|
||||
static_assert(C4F_INPUT_BUTTON_CIRCLE == ORBIS_PAD_BUTTON_CIRCLE, "Back button");
|
||||
static_assert(C4F_INPUT_BUTTON_OPTIONS == ORBIS_PAD_BUTTON_OPTIONS, "Menu button");
|
||||
static_assert(offsetof(OrbisPadData, unknown) + 3 == 108, "remote-control key byte");
|
||||
|
||||
enum { C4F_INPUT_WAITING, C4F_INPUT_READY, C4F_INPUT_DISCONNECTED,
|
||||
C4F_INPUT_OPEN_FAILED, C4F_INPUT_READ_FAILED, C4F_INPUT_USER_FAILED };
|
||||
|
||||
typedef struct {
|
||||
int initialized, handles[2], state[2];
|
||||
uint32_t error[2], buttons[2];
|
||||
uint32_t remoteLastRawButtons;
|
||||
uint8_t remoteLastKey;
|
||||
} C4fNativeInput;
|
||||
|
||||
static const char *const c4fInputNames[] = { "DS4", "TV remote" };
|
||||
static const char *const c4fInputStates[] = {
|
||||
"waiting", "ready", "disconnected", "open failed", "read failed", "user unavailable"
|
||||
};
|
||||
|
||||
static void c4fNativeInputState(C4fNativeInput *input, int source, int state, int result)
|
||||
{
|
||||
if (input->state[source] != state || input->error[source] != (uint32_t)result)
|
||||
printf("[c4f-launcher] %s input: %s (0x%08x)\n",
|
||||
c4fInputNames[source], c4fInputStates[state], (unsigned)result);
|
||||
input->state[source] = state;
|
||||
input->error[source] = (uint32_t)result;
|
||||
if (state != C4F_INPUT_READY) input->buttons[source] = 0;
|
||||
}
|
||||
|
||||
static int c4fNativeInitialUser(void *context, int32_t *user)
|
||||
{
|
||||
int result = sceUserServiceGetInitialUser(user);
|
||||
if (result) c4fNativeInputState((C4fNativeInput *)context, 0, C4F_INPUT_USER_FAILED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
static int c4fNativeOpen(void *context, int32_t user, int port, int *owned)
|
||||
{
|
||||
C4fNativeInput *input = (C4fNativeInput *)context;
|
||||
int source = port == ORBIS_PAD_PORT_TYPE_STANDARD ? 0 : 1;
|
||||
*owned = 0;
|
||||
if (!input->initialized) {
|
||||
int result = scePadInit();
|
||||
if (result) {
|
||||
c4fNativeInputState(input, source, C4F_INPUT_OPEN_FAILED, result);
|
||||
return -1;
|
||||
}
|
||||
input->initialized = 1;
|
||||
}
|
||||
int handle = scePadOpen(user, port, 0, NULL);
|
||||
if ((uint32_t)handle == C4F_PAD_ERROR_ALREADY_OPENED)
|
||||
handle = scePadGetHandle(user, port, 0);
|
||||
else if (handle >= 0)
|
||||
*owned = 1;
|
||||
input->handles[source] = handle;
|
||||
if (handle < 0) c4fNativeInputState(input, source, C4F_INPUT_OPEN_FAILED, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
static int c4fNativeRead(void *context, int handle, uint32_t *buttons, int *connected)
|
||||
{
|
||||
C4fNativeInput *input = (C4fNativeInput *)context;
|
||||
int source = handle == input->handles[0] ? 0 : 1;
|
||||
OrbisPadData sample;
|
||||
memset(&sample, 0, sizeof(sample));
|
||||
int result = scePadReadState(handle, &sample);
|
||||
uint32_t mappedButtons = sample.buttons;
|
||||
if (source == C4F_INPUT_REMOTE && result == 0) {
|
||||
/* The SDK's byte108 is deviceUniqueData[0] in the reverse PS4 layout.
|
||||
* PS5 implementations use it for remote Enter/Back/Menu even when the
|
||||
* device-data length is zero. PS4 key decoding still needs hardware
|
||||
* confirmation. */
|
||||
uint8_t keyCode = sample.unknown[3];
|
||||
if (keyCode || sample.buttons) {
|
||||
input->remoteLastKey = keyCode;
|
||||
input->remoteLastRawButtons = sample.buttons;
|
||||
}
|
||||
mappedButtons = c4fInputRemoteButtons(sample.buttons, keyCode);
|
||||
}
|
||||
*buttons = result == 0 && sample.connected ? mappedButtons : 0;
|
||||
*connected = result == 0 && sample.connected;
|
||||
c4fNativeInputState(input, source, result ? C4F_INPUT_READ_FAILED :
|
||||
sample.connected ? C4F_INPUT_READY : C4F_INPUT_DISCONNECTED, result);
|
||||
input->buttons[source] = *buttons;
|
||||
return result;
|
||||
}
|
||||
|
||||
static void c4fNativeClose(void *, int handle) { scePadClose(handle); }
|
||||
|
||||
static void c4fNativeInputNote(const C4fNativeInput *input, char *note, size_t size)
|
||||
{
|
||||
snprintf(note, size, "%s: %s (0x%08x); %s: %s (0x%08x), last key 0x%02x / raw 0x%08x",
|
||||
c4fInputNames[0], c4fInputStates[input->state[0]],
|
||||
input->error[0] ? input->error[0] : input->buttons[0],
|
||||
c4fInputNames[1], c4fInputStates[input->state[1]],
|
||||
input->error[1] ? input->error[1] : input->buttons[1],
|
||||
(unsigned)input->remoteLastKey, input->remoteLastRawButtons);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const size_t frameBytes = (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(uint32_t);
|
||||
@@ -184,9 +296,11 @@ int main(void)
|
||||
OrbisUserServiceInitializeParams params = {};
|
||||
params.priority = ORBIS_KERNEL_PRIO_FIFO_LOWEST;
|
||||
sceUserServiceInitialize(¶ms);
|
||||
int32_t user = -1, pad = -1;
|
||||
if (scePadInit() == 0 && sceUserServiceGetInitialUser(&user) == 0)
|
||||
pad = scePadOpen(user, ORBIS_PAD_PORT_TYPE_STANDARD, 0, NULL);
|
||||
C4fNativeInput nativeInput = {};
|
||||
nativeInput.handles[0] = nativeInput.handles[1] = -1;
|
||||
C4fInputOps inputOps = { &nativeInput, c4fNativeInitialUser, c4fNativeOpen, c4fNativeRead, c4fNativeClose };
|
||||
C4fInput inputs;
|
||||
c4fInputInit(&inputs, &inputOps);
|
||||
c4fScreen.running = -1; c4fScreen.busy = 1; c4fScreen.autorun = C4F_AUTORUN_UNKNOWN;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Checking Control4Free...");
|
||||
/* Read before leaving the sandbox: /app0 is only visible from inside it. */
|
||||
@@ -201,50 +315,60 @@ int main(void)
|
||||
#endif
|
||||
pthread_t worker;
|
||||
int workerStarted = pthread_create(&worker, NULL, c4fWorker, NULL) == 0;
|
||||
if (!workerStarted || pad < 0) {
|
||||
if (!workerStarted) {
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
c4fScreen.locked = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Could not initialize launcher. Close with PS and retry.");
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
}
|
||||
sceSystemServiceHideSplashScreen();
|
||||
uint32_t previous = 0;
|
||||
uint64_t lastInputAt = c4fLauncherTimeMs();
|
||||
int buffer = 0, done = 0, drawn = 0, shown[2] = { 0, 0 };
|
||||
int64_t frame = 1;
|
||||
C4fLauncherScreen drawnState;
|
||||
memset(&drawnState, 0, sizeof(drawnState));
|
||||
while (!done) {
|
||||
OrbisPadData input;
|
||||
memset(&input, 0, sizeof(input));
|
||||
uint32_t buttons = pad >= 0 && scePadReadState(pad, &input) == 0 && input.connected ? input.buttons : 0;
|
||||
uint32_t pressed = buttons & ~previous;
|
||||
previous = buttons;
|
||||
uint64_t now = c4fLauncherTimeMs();
|
||||
/* A suspended app must not resume a pending confirmation or reuse a
|
||||
* held navigation button. Do not guess unreversed focus API signatures. */
|
||||
int resumed = now > lastInputAt && now - lastInputAt > 1000;
|
||||
if (resumed) c4fInputReset(&inputs, now);
|
||||
lastInputAt = now;
|
||||
C4fInputFrame input;
|
||||
c4fInputPoll(&inputs, now, &input);
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
if (!c4fScreen.busy) {
|
||||
if (pressed & ORBIS_PAD_BUTTON_CIRCLE) {
|
||||
if (c4fScreen.confirmStop) c4fScreen.confirmStop = 0;
|
||||
else done = 1;
|
||||
} else if (pressed & ORBIS_PAD_BUTTON_CROSS) {
|
||||
if (c4fScreen.confirmStop) {
|
||||
c4fCommand = C4F_DO_STOP; c4fScreen.confirmStop = 0; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Stopping and releasing controllers...");
|
||||
} else if (!c4fScreen.locked && c4fScreen.running != 1) {
|
||||
/* Until auto-start is on, Cross sets it up and starts it. */
|
||||
int setUp = c4fScreen.autorun != C4F_AUTORUN_ON;
|
||||
c4fCommand = setUp ? C4F_DO_SET_UP : C4F_DO_START; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
|
||||
setUp ? "Setting up Control4Free. Please wait..." : "Starting Control4Free. Please wait...");
|
||||
}
|
||||
} else if ((pressed & ORBIS_PAD_BUTTON_TRIANGLE) && !c4fScreen.confirmStop &&
|
||||
c4fScreen.autorun != C4F_AUTORUN_UNKNOWN) {
|
||||
int off = c4fScreen.autorun == C4F_AUTORUN_ON;
|
||||
c4fCommand = off ? C4F_DO_AUTORUN_OFF : C4F_DO_AUTORUN_ON; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
|
||||
off ? "Turning auto-start off..." : "Turning auto-start on...");
|
||||
} else if ((pressed & ORBIS_PAD_BUTTON_SQUARE) && c4fScreen.running == 1) c4fScreen.confirmStop = 1;
|
||||
c4fNativeInputNote(&nativeInput, c4fScreen.inputNote, sizeof(c4fScreen.inputNote));
|
||||
if (resumed) {
|
||||
c4fScreen.actionMenu = c4fScreen.confirmStop = c4fScreen.confirmMenu = 0;
|
||||
c4fScreen.actionFocus = C4F_ACTION_NONE;
|
||||
}
|
||||
uint32_t standardUnhandled = 0, remoteUnhandled = 0;
|
||||
#ifdef C4F_DIAG
|
||||
const int diagnostic = 1;
|
||||
#else
|
||||
const int diagnostic = 0;
|
||||
#endif
|
||||
C4fLauncherAction action = c4fNavigate(&c4fScreen, input.standard.pressed, input.remote.pressed,
|
||||
diagnostic, &standardUnhandled, &remoteUnhandled);
|
||||
if (action == C4F_ACTION_CLOSE) done = 1;
|
||||
else if (action == C4F_ACTION_START) {
|
||||
/* Until auto-start is on, Start sets it up and starts it. */
|
||||
int setUp = c4fScreen.autorun != C4F_AUTORUN_ON;
|
||||
c4fCommand = setUp ? C4F_DO_SET_UP : C4F_DO_START; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
|
||||
setUp ? "Setting up Control4Free. Please wait..." : "Starting Control4Free. Please wait...");
|
||||
} else if (action == C4F_ACTION_STOP) {
|
||||
c4fCommand = C4F_DO_STOP; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Stopping and releasing controllers...");
|
||||
} else if (action == C4F_ACTION_AUTORUN) {
|
||||
int off = c4fScreen.autorun == C4F_AUTORUN_ON;
|
||||
c4fCommand = off ? C4F_DO_AUTORUN_OFF : C4F_DO_AUTORUN_ON; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
|
||||
off ? "Turning auto-start off..." : "Turning auto-start on...");
|
||||
}
|
||||
#ifdef C4F_DIAG
|
||||
/* Pages and scrolling work while busy too. */
|
||||
uint32_t pressed = standardUnhandled | remoteUnhandled;
|
||||
int page = c4fScreen.diagPage;
|
||||
if (pressed & (ORBIS_PAD_BUTTON_R1 | ORBIS_PAD_BUTTON_RIGHT)) c4fScreen.diagPage = (page + 1) % C4F_DIAG_PAGES;
|
||||
if (pressed & (ORBIS_PAD_BUTTON_L1 | ORBIS_PAD_BUTTON_LEFT)) c4fScreen.diagPage = (page + C4F_DIAG_PAGES - 1) % C4F_DIAG_PAGES;
|
||||
@@ -254,7 +378,7 @@ int main(void)
|
||||
at += pressed & ORBIS_PAD_BUTTON_UP ? -10 : 10;
|
||||
c4fScreen.diagScroll[page] = at >= max ? C4F_DIAG_END : at < 0 ? 0 : at;
|
||||
}
|
||||
if (pressed & ORBIS_PAD_BUTTON_OPTIONS) c4fDiagCapture(4000);
|
||||
if (action == C4F_ACTION_CAPTURE || (pressed & ORBIS_PAD_BUTTON_OPTIONS)) c4fDiagCapture(4000);
|
||||
#endif
|
||||
C4fLauncherScreen snapshot = c4fScreen;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
@@ -286,7 +410,7 @@ int main(void)
|
||||
#ifdef C4F_DIAG
|
||||
c4fDiagStop();
|
||||
#endif
|
||||
if (pad >= 0) scePadClose(pad);
|
||||
c4fInputClose(&inputs);
|
||||
sceNetCtlTerm();
|
||||
c4fCloseVideo();
|
||||
free(canvas);
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
#include "navigation.h"
|
||||
#include "autorun.h"
|
||||
#include "input.h"
|
||||
|
||||
#define C4F_NAV_BACK C4F_INPUT_BUTTON_CIRCLE
|
||||
#define C4F_NAV_OK C4F_INPUT_BUTTON_CROSS
|
||||
#define C4F_NAV_PREVIOUS (C4F_INPUT_BUTTON_UP | C4F_INPUT_BUTTON_LEFT)
|
||||
#define C4F_NAV_NEXT (C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_RIGHT)
|
||||
#define C4F_NAV_DIRECTIONS (C4F_NAV_PREVIOUS | C4F_NAV_NEXT)
|
||||
|
||||
int c4fActionAvailable(const C4fLauncherScreen *screen, C4fLauncherAction action, int diagnostic)
|
||||
{
|
||||
switch (action) {
|
||||
case C4F_ACTION_RESUME: return 1;
|
||||
case C4F_ACTION_START: return !screen->busy && !screen->locked && screen->running != 1;
|
||||
case C4F_ACTION_AUTORUN:
|
||||
return !screen->busy && !screen->locked && screen->autorun != C4F_AUTORUN_UNKNOWN;
|
||||
case C4F_ACTION_STOP: return !screen->busy && !screen->locked && screen->running == 1;
|
||||
case C4F_ACTION_CAPTURE: return !!diagnostic;
|
||||
case C4F_ACTION_CLOSE: return !screen->busy;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int c4fActionList(const C4fLauncherScreen *screen, int diagnostic,
|
||||
C4fLauncherAction *actions, int capacity)
|
||||
{
|
||||
const C4fLauncherAction order[] = { C4F_ACTION_RESUME, C4F_ACTION_START,
|
||||
C4F_ACTION_AUTORUN, C4F_ACTION_STOP, C4F_ACTION_CAPTURE, C4F_ACTION_CLOSE };
|
||||
int count = 0;
|
||||
for (unsigned i = 0; i < sizeof(order) / sizeof(order[0]); i++) {
|
||||
if (!c4fActionAvailable(screen, order[i], diagnostic)) continue;
|
||||
if (actions && count < capacity) actions[count] = order[i];
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const char *c4fActionLabel(const C4fLauncherScreen *screen, C4fLauncherAction action)
|
||||
{
|
||||
switch (action) {
|
||||
case C4F_ACTION_RESUME: return "Resume";
|
||||
case C4F_ACTION_START: return screen->autorun == C4F_AUTORUN_ON ? "Start Control4Free" : "Set up and start";
|
||||
case C4F_ACTION_AUTORUN:
|
||||
if (screen->autorun == C4F_AUTORUN_ON) return "Turn auto-start off";
|
||||
if (screen->autorun == C4F_AUTORUN_OUTDATED) return "Update auto-start";
|
||||
return "Turn auto-start on";
|
||||
case C4F_ACTION_STOP: return "Stop Control4Free";
|
||||
case C4F_ACTION_CLOSE: return "Close app";
|
||||
case C4F_ACTION_CAPTURE: return "Capture kernel log";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
static C4fLauncherAction c4fDefaultAction(const C4fLauncherScreen *screen, int diagnostic)
|
||||
{
|
||||
return c4fActionAvailable(screen, C4F_ACTION_START, diagnostic) ? C4F_ACTION_START : C4F_ACTION_RESUME;
|
||||
}
|
||||
|
||||
static int c4fDirection(uint32_t buttons)
|
||||
{
|
||||
int previous = !!(buttons & C4F_NAV_PREVIOUS), next = !!(buttons & C4F_NAV_NEXT);
|
||||
return previous == next ? 0 : next ? 1 : -1;
|
||||
}
|
||||
|
||||
static void c4fConsume(uint32_t *standardUnhandled, uint32_t *remoteUnhandled)
|
||||
{
|
||||
if (standardUnhandled) *standardUnhandled = 0;
|
||||
if (remoteUnhandled) *remoteUnhandled = 0;
|
||||
}
|
||||
|
||||
static void c4fCancelConfirmation(C4fLauncherScreen *screen)
|
||||
{
|
||||
screen->confirmStop = 0;
|
||||
screen->confirmChoice = 0;
|
||||
screen->confirmMenu = 0;
|
||||
}
|
||||
|
||||
C4fLauncherAction c4fNavigate(C4fLauncherScreen *screen, uint32_t standardPressed,
|
||||
uint32_t remotePressed, int diagnostic,
|
||||
uint32_t *standardUnhandled, uint32_t *remoteUnhandled)
|
||||
{
|
||||
if (standardUnhandled) *standardUnhandled = standardPressed;
|
||||
if (remoteUnhandled) *remoteUnhandled = remotePressed;
|
||||
uint32_t pressed = standardPressed | remotePressed;
|
||||
if (screen->confirmStop) {
|
||||
c4fConsume(standardUnhandled, remoteUnhandled);
|
||||
if (!c4fActionAvailable(screen, C4F_ACTION_STOP, diagnostic)) {
|
||||
c4fCancelConfirmation(screen);
|
||||
return C4F_ACTION_NONE;
|
||||
}
|
||||
if (pressed & C4F_NAV_BACK) {
|
||||
c4fCancelConfirmation(screen);
|
||||
return C4F_ACTION_RESUME;
|
||||
}
|
||||
/* The DS4 shortcut displays Cross-to-stop. A remote's first event
|
||||
* instead reveals an explicit, Cancel-focused selection and cannot
|
||||
* accept the confirmation that another source opened. */
|
||||
if (!screen->confirmMenu && remotePressed) {
|
||||
screen->confirmMenu = 1;
|
||||
screen->confirmChoice = 0;
|
||||
return C4F_ACTION_NONE;
|
||||
}
|
||||
if (pressed & C4F_NAV_DIRECTIONS) {
|
||||
if (!screen->confirmMenu) {
|
||||
screen->confirmMenu = 1;
|
||||
screen->confirmChoice = 0;
|
||||
}
|
||||
int direction = c4fDirection(pressed);
|
||||
if (direction) screen->confirmChoice = direction > 0;
|
||||
return C4F_ACTION_NONE;
|
||||
}
|
||||
if (pressed & C4F_NAV_OK) {
|
||||
int stop = screen->confirmChoice;
|
||||
c4fCancelConfirmation(screen);
|
||||
return stop ? C4F_ACTION_STOP : C4F_ACTION_RESUME;
|
||||
}
|
||||
return C4F_ACTION_NONE;
|
||||
}
|
||||
if (screen->actionMenu) {
|
||||
c4fConsume(standardUnhandled, remoteUnhandled);
|
||||
if (!c4fActionAvailable(screen, (C4fLauncherAction)screen->actionFocus, diagnostic)) {
|
||||
screen->actionFocus = c4fDefaultAction(screen, diagnostic);
|
||||
return C4F_ACTION_NONE;
|
||||
}
|
||||
if (pressed & C4F_NAV_BACK) {
|
||||
screen->actionMenu = 0;
|
||||
return C4F_ACTION_RESUME;
|
||||
}
|
||||
if (pressed & C4F_NAV_DIRECTIONS) {
|
||||
int direction = c4fDirection(pressed);
|
||||
if (direction) {
|
||||
C4fLauncherAction actions[6];
|
||||
int count = c4fActionList(screen, diagnostic, actions, 6), at = 0;
|
||||
while (at < count && (int)actions[at] != screen->actionFocus) at++;
|
||||
screen->actionFocus = actions[(at + count + direction) % count];
|
||||
}
|
||||
return C4F_ACTION_NONE;
|
||||
}
|
||||
if (pressed & C4F_NAV_OK) {
|
||||
C4fLauncherAction action = (C4fLauncherAction)screen->actionFocus;
|
||||
screen->actionMenu = 0;
|
||||
if (action == C4F_ACTION_STOP) {
|
||||
screen->confirmStop = 1;
|
||||
screen->confirmChoice = 0;
|
||||
screen->confirmMenu = 1;
|
||||
return C4F_ACTION_NONE;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
return C4F_ACTION_NONE;
|
||||
}
|
||||
/* A TV remote needs an action menu because it has no Triangle or Square.
|
||||
* In diagnostic mode its arrows keep the existing paging and scrolling. */
|
||||
if ((remotePressed & (C4F_NAV_OK | C4F_INPUT_BUTTON_OPTIONS)) ||
|
||||
(!diagnostic && (pressed & C4F_NAV_DIRECTIONS))) {
|
||||
c4fConsume(standardUnhandled, remoteUnhandled);
|
||||
screen->actionMenu = 1;
|
||||
screen->actionFocus = c4fDefaultAction(screen, diagnostic);
|
||||
return C4F_ACTION_NONE;
|
||||
}
|
||||
if (screen->busy) return C4F_ACTION_NONE;
|
||||
if (pressed & C4F_NAV_BACK) {
|
||||
c4fConsume(standardUnhandled, remoteUnhandled);
|
||||
return C4F_ACTION_CLOSE;
|
||||
}
|
||||
if (standardPressed & C4F_NAV_OK)
|
||||
return c4fActionAvailable(screen, C4F_ACTION_START, diagnostic) ? C4F_ACTION_START : C4F_ACTION_NONE;
|
||||
if ((standardPressed & C4F_INPUT_BUTTON_TRIANGLE) &&
|
||||
c4fActionAvailable(screen, C4F_ACTION_AUTORUN, diagnostic)) return C4F_ACTION_AUTORUN;
|
||||
if ((standardPressed & C4F_INPUT_BUTTON_SQUARE) && c4fActionAvailable(screen, C4F_ACTION_STOP, diagnostic)) {
|
||||
screen->confirmStop = 1;
|
||||
screen->confirmChoice = 1;
|
||||
screen->confirmMenu = 0;
|
||||
c4fConsume(standardUnhandled, remoteUnhandled);
|
||||
}
|
||||
return C4F_ACTION_NONE;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef C4F_LAUNCHER_NAVIGATION_H
|
||||
#define C4F_LAUNCHER_NAVIGATION_H
|
||||
#include "screen.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* Returns the number of available actions, writing up to capacity entries. */
|
||||
int c4fActionList(const C4fLauncherScreen *screen, int diagnostic,
|
||||
C4fLauncherAction *actions, int capacity);
|
||||
int c4fActionAvailable(const C4fLauncherScreen *screen, C4fLauncherAction action, int diagnostic);
|
||||
const char *c4fActionLabel(const C4fLauncherScreen *screen, C4fLauncherAction action);
|
||||
/* Only rising edges are accepted. The remaining bits preserve diagnostic
|
||||
* paging/scrolling outside the menu, without moving both a page and focus. */
|
||||
C4fLauncherAction c4fNavigate(C4fLauncherScreen *screen, uint32_t standardPressed,
|
||||
uint32_t remotePressed, int diagnostic,
|
||||
uint32_t *standardUnhandled, uint32_t *remoteUnhandled);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
+83
-20
@@ -9,6 +9,7 @@
|
||||
#include "autorun.h"
|
||||
#include "draw.h"
|
||||
#include "logo.h"
|
||||
#include "navigation.h"
|
||||
#include "qrcodegen.h"
|
||||
|
||||
/* Page CSS pixels to TV pixels. */
|
||||
@@ -443,18 +444,90 @@ static C4fLook c4fLookFor(const C4fLauncherScreen *s, char *meta, size_t metaSiz
|
||||
meta, C4F_OK, C4F_ACCENT, 1 };
|
||||
} else if (s->running == 0 && s->autorun == C4F_AUTORUN_ON) {
|
||||
look = (C4fLook){ "Stopped", "Start Control4Free",
|
||||
"It starts with GoldHEN after each restart. To start it now, turn on GoldHEN's PayLoader "
|
||||
"and press Cross.",
|
||||
"It starts with GoldHEN after each restart. To start now, enable GoldHEN's PayLoader "
|
||||
"Server and press Cross.",
|
||||
"Not running", C4F_FAINT, C4F_FAINT, 0 };
|
||||
} else if (s->running == 0) {
|
||||
look = (C4fLook){ "Stopped", "Set up Control4Free",
|
||||
"Press Cross once. GoldHEN will start Control4Free each time it loads, and it starts "
|
||||
"right away if GoldHEN's PayLoader is on.",
|
||||
"Press Cross to set up auto-start. To start Control4Free now, enable GoldHEN's "
|
||||
"PayLoader Server.",
|
||||
"Not running", C4F_FAINT, C4F_FAINT, 0 };
|
||||
}
|
||||
return look;
|
||||
}
|
||||
|
||||
/* The TV remote and controller share a visible action menu. Keep its rendering
|
||||
* here so the normal and diagnostic screens use the same focus and confirmation. */
|
||||
void c4fDrawActionOverlay(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
{
|
||||
if (!s->confirmStop && !s->actionMenu) return;
|
||||
C4fCanvas c = { pixels, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
|
||||
const float dw = 980;
|
||||
const float dx = (c.w - dw) / 2;
|
||||
int diagnostic = 0;
|
||||
#ifdef C4F_DIAG
|
||||
diagnostic = 1;
|
||||
#endif
|
||||
C4fLauncherAction actions[7];
|
||||
int count = s->confirmStop ? 0 : c4fActionList(s, diagnostic, actions, 7);
|
||||
const float dh = s->confirmStop ? (s->confirmMenu ? 410 : 330) : 220 + count * 72;
|
||||
const float dy = (c.h - dh) / 2;
|
||||
c4fFillRect(&c, 0, 0, c.w, c.h, C4F_RGBA(2, 11, 31, .65f));
|
||||
c4fShadowRoundRect(&c, dx, dy, dw, dh, 6 * C4F_S, 0, 16 * C4F_S,
|
||||
34 * C4F_S, -18 * C4F_S, C4F_RGBA(0, 0, 0, .9f));
|
||||
c4fPanel(&c, dx, dy, dw, dh, C4F_RGBA(2, 11, 31, .97f), C4F_LINE);
|
||||
c4fBandRoundRect(&c, dx, dy, dw, dh, 6 * C4F_S, 3 * C4F_S, C4F_ACCENT);
|
||||
|
||||
if (s->confirmStop) {
|
||||
c4fText(&c, C4F_FONT_LIGHT, 48, dx + 48, dy + 92, C4F_TEXT, "Stop Control4Free?");
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 26, dx + 48, dy + 148, dw - 96, 39, 2, C4F_MUTED,
|
||||
"Every controller disconnects. You can start Control4Free again from this screen.");
|
||||
if (!s->confirmMenu) {
|
||||
static const C4fHint hints[2] = { { C4F_KEY_CROSS, "Stop" }, { C4F_KEY_CIRCLE, "Cancel" } };
|
||||
c4fHints(&c, dx + dw - 48, dy + dh - 58, hints, 2);
|
||||
return;
|
||||
}
|
||||
const float bw = 240, gap = 24, by = dy + 235;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
float bx = dx + dw - 48 - 2 * bw - gap + i * (bw + gap);
|
||||
int focused = s->confirmChoice == i;
|
||||
c4fFillRoundRect(&c, bx, by, bw, 64, 6 * C4F_S,
|
||||
focused ? C4F_RGBA(40, 103, 205, .85f) : C4F_TILE);
|
||||
c4fInsetRoundRect(&c, bx, by, bw, 64, 6 * C4F_S,
|
||||
focused ? 3 : 1 * C4F_S, focused ? C4F_ACCENT : C4F_LINE);
|
||||
c4fTextCentered(&c, C4F_FONT_REGULAR, 30, bx + bw / 2, by + 42,
|
||||
focused ? C4F_TEXT : C4F_MUTED, i ? "Stop" : "Cancel");
|
||||
}
|
||||
c4fText(&c, C4F_FONT_LIGHT, 23, dx + 48, dy + dh - 48, C4F_MUTED,
|
||||
"Arrows / D-pad: select OK / Cross: confirm Back / Circle: cancel");
|
||||
return;
|
||||
}
|
||||
|
||||
c4fText(&c, C4F_FONT_LIGHT, 48, dx + 48, dy + 84, C4F_TEXT, "Actions");
|
||||
c4fText(&c, C4F_FONT_LIGHT, 24, dx + 48, dy + 125, C4F_MUTED,
|
||||
s->busy ? "Working... You can return to the screen." : "Use arrows or the D-pad to choose an action.");
|
||||
int focus = 0;
|
||||
for (int i = 0; i < count; i++) if ((int)actions[i] == s->actionFocus) focus = i;
|
||||
for (int i = 0; i < count; i++) {
|
||||
const float rx = dx + 32, ry = dy + 153 + i * 72, rw = dw - 64;
|
||||
int focused = i == focus;
|
||||
c4fFillRoundRect(&c, rx, ry, rw, 64, 6 * C4F_S,
|
||||
focused ? C4F_RGBA(40, 103, 205, .85f) : C4F_RGBA(255, 255, 255, .04f));
|
||||
c4fInsetRoundRect(&c, rx, ry, rw, 64, 6 * C4F_S,
|
||||
focused ? 3 : 1 * C4F_S, focused ? C4F_ACCENT : C4F_LINE_SOFT);
|
||||
c4fText(&c, C4F_FONT_REGULAR, 30, rx + 24, ry + 42,
|
||||
focused ? C4F_TEXT : C4F_MUTED, c4fActionLabel(s, actions[i]));
|
||||
if (focused) {
|
||||
float cx = rx + rw - 30, cy = ry + 32;
|
||||
c4fSegment(&c, cx - 5, cy - 9, cx + 4, cy, 3, C4F_TEXT);
|
||||
c4fSegment(&c, cx + 4, cy, cx - 5, cy + 9, 3, C4F_TEXT);
|
||||
}
|
||||
}
|
||||
static const C4fHint hints[2] = { { C4F_KEY_CROSS, "Select" }, { C4F_KEY_CIRCLE, "Back" } };
|
||||
c4fText(&c, C4F_FONT_LIGHT, 23, dx + 48, dy + dh - 32, C4F_MUTED, "TV remote: OK select / Back return");
|
||||
c4fHints(&c, dx + dw - 48, dy + dh - 40, hints, 2);
|
||||
}
|
||||
|
||||
void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
{
|
||||
C4fCanvas c = { pixels, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
|
||||
@@ -529,7 +602,7 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
if (mismatch) {
|
||||
snprintf(versions, sizeof(versions), "Installed %s; running %s", C4F_LAUNCHER_VERSION, s->runningVersion);
|
||||
panelTitle = versions;
|
||||
panelText = "To apply this app's version now, press Square, confirm the stop with Cross, then press Cross to start. This disconnects all players.";
|
||||
panelText = "Press Square, then Cross to stop, and Cross again to start this app's version. All players disconnect.";
|
||||
panelDot = C4F_WARN;
|
||||
} else if (s->autorun == C4F_AUTORUN_ON) {
|
||||
panelTitle = "Starts by itself with GoldHEN";
|
||||
@@ -538,12 +611,11 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
panelDot = C4F_OK;
|
||||
} else if (s->autorun == C4F_AUTORUN_OUTDATED) {
|
||||
panelTitle = "Auto-start uses an older Control4Free";
|
||||
panelText = "Press Triangle to switch GoldHEN's auto-start to this version. It takes effect after the "
|
||||
"next restart.";
|
||||
panelText = "Press Triangle to update auto-start. GoldHEN uses this version after the next restart.";
|
||||
panelDot = C4F_WARN;
|
||||
} else if (s->autorun == C4F_AUTORUN_OFF) {
|
||||
panelTitle = "Auto-start is off";
|
||||
panelText = "Press Triangle and GoldHEN starts Control4Free each time it loads, with no PC needed.";
|
||||
panelText = "Press Triangle to turn auto-start on. GoldHEN then starts Control4Free each time it loads.";
|
||||
} else {
|
||||
panelTitle = "Auto-start status unknown";
|
||||
panelText = s->autorunNote[0] ? s->autorunNote : "Checking with GoldHEN...";
|
||||
@@ -557,6 +629,8 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
panelText);
|
||||
|
||||
/* .home-foot and the button guide. */
|
||||
if (!s->confirmStop && !s->actionMenu)
|
||||
c4fText(&c, C4F_FONT_LIGHT, 23, left, 973, C4F_MUTED, "TV remote: OK opens actions / arrows select / Back returns");
|
||||
c4fText(&c, C4F_FONT_LIGHT, 20, left, 1030, C4F_FAINT, "Control4Free " C4F_LAUNCHER_VERSION);
|
||||
if (!s->confirmStop && !s->busy) {
|
||||
C4fHint hints[4];
|
||||
@@ -579,18 +653,7 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
c4fHints(&c, right, 1022, hints, n);
|
||||
}
|
||||
|
||||
if (s->confirmStop) {
|
||||
const float dw = 980, dh = 330, dx = (c.w - dw) / 2, dy = (c.h - dh) / 2;
|
||||
static const C4fHint hints[2] = { { C4F_KEY_CROSS, "Stop" }, { C4F_KEY_CIRCLE, "Cancel" } };
|
||||
c4fFillRect(&c, 0, 0, c.w, c.h, C4F_RGBA(2, 11, 31, .6f));
|
||||
c4fShadowRoundRect(&c, dx, dy, dw, dh, 6 * C4F_S, 0, 16 * C4F_S, 34 * C4F_S, -18 * C4F_S, C4F_RGBA(0, 0, 0, .9f));
|
||||
/* A little more opaque than --panel-strong, so the tiles do not ghost through. */
|
||||
c4fPanel(&c, dx, dy, dw, dh, C4F_RGBA(2, 11, 31, .94f), C4F_LINE);
|
||||
c4fText(&c, C4F_FONT_LIGHT, 48, dx + 48, dy + 96, C4F_TEXT, "Stop Control4Free?");
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 26, dx + 48, dy + 152, dw - 96, 39, 2, C4F_MUTED,
|
||||
"Every controller disconnects. You can start Control4Free again from this screen.");
|
||||
c4fHints(&c, dx + dw - 48, dy + dh - 58, hints, 2);
|
||||
}
|
||||
c4fDrawActionOverlay(pixels, s);
|
||||
}
|
||||
|
||||
void c4fDrawIcon(uint32_t *pixels, int size)
|
||||
|
||||
+14
-1
@@ -7,10 +7,21 @@
|
||||
#endif
|
||||
#define C4F_SCREEN_WIDTH 1920
|
||||
#define C4F_SCREEN_HEIGHT 1080
|
||||
typedef enum {
|
||||
C4F_ACTION_NONE,
|
||||
C4F_ACTION_RESUME,
|
||||
C4F_ACTION_START,
|
||||
C4F_ACTION_AUTORUN,
|
||||
C4F_ACTION_STOP,
|
||||
C4F_ACTION_CLOSE,
|
||||
C4F_ACTION_CAPTURE
|
||||
} C4fLauncherAction;
|
||||
typedef struct {
|
||||
int running, busy, controllers, confirmStop, locked;
|
||||
int actionMenu, actionFocus;
|
||||
int confirmChoice, confirmMenu; /* 0 Cancel / 1 Stop; menu-origin confirmation. */
|
||||
int autorun; /* C4F_AUTORUN_* from autorun.h */
|
||||
char address[64], message[160], autorunNote[96], runningVersion[32];
|
||||
char address[64], message[160], autorunNote[96], runningVersion[32], inputNote[160];
|
||||
#ifdef C4F_DIAG
|
||||
unsigned diagGeneration; /* bumped when a diagnostic page changes */
|
||||
int diagPage, diagScroll[8];
|
||||
@@ -24,6 +35,8 @@ int c4fScreenInit(void);
|
||||
/* Draws a whole frame. Blending reads pixels back, so pass cached memory,
|
||||
* never the write-combined framebuffer. */
|
||||
void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *state);
|
||||
/* Shared actions and stop-confirmation overlay for both package variants. */
|
||||
void c4fDrawActionOverlay(uint32_t *pixels, const C4fLauncherScreen *state);
|
||||
/* The package icon (size x size), in the same style. */
|
||||
void c4fDrawIcon(uint32_t *pixels, int size);
|
||||
#ifdef __cplusplus
|
||||
|
||||
+5
-5
@@ -239,10 +239,10 @@ int c4fLauncherStart(const unsigned char *payload, size_t payloadSize, char *mes
|
||||
const char *targets[2];
|
||||
int count = c4fTargets(targets), fd = -1;
|
||||
for (int i = 0; i < count && fd < 0; i++) fd = c4fConnect(targets[i], C4F_PAYLOADER_PORT);
|
||||
if (fd < 0) { snprintf(message, size, "PayLoader did not answer (errno %d). Turn it on in GoldHEN, then press Cross again.", errno); return -1; }
|
||||
if (fd < 0) { snprintf(message, size, "PayLoader did not answer (errno %d). Enable PayLoader Server in GoldHEN > Server Settings, then retry Start.", errno); return -1; }
|
||||
int failed = c4fSendAll(fd, payload, payloadSize, c4fLauncherTimeMs() + 10000);
|
||||
shutdown(fd, SHUT_WR); close(fd);
|
||||
if (failed) { snprintf(message, size, "The transfer to PayLoader broke off. Press Cross to try again."); return -2; }
|
||||
if (failed) { snprintf(message, size, "The transfer to PayLoader broke off. Retry Start."); return -2; }
|
||||
uint64_t deadline;
|
||||
deadline = c4fLauncherTimeMs() + 20000;
|
||||
while (c4fLauncherTimeMs() < deadline) {
|
||||
@@ -251,7 +251,7 @@ int c4fLauncherStart(const unsigned char *payload, size_t payloadSize, char *mes
|
||||
}
|
||||
usleep(250000);
|
||||
}
|
||||
snprintf(message, size, "Sent, but Control4Free did not start (%s). Press Cross to try again.", c4fProblem);
|
||||
snprintf(message, size, "Sent, but Control4Free did not start (%s). Retry Start.", c4fProblem);
|
||||
/* Sending again is safe: a copy that did start holds the payload's
|
||||
* instance lock, so a second one quits at once. */
|
||||
return -2;
|
||||
@@ -261,7 +261,7 @@ int c4fLauncherStop(char *message, size_t size)
|
||||
{
|
||||
C4fServiceStatus status;
|
||||
int probe = c4fLauncherProbe(&status);
|
||||
if (probe == 0) { snprintf(message, size, "Stopped. Press Cross to start it again."); return 0; }
|
||||
if (probe == 0) { snprintf(message, size, "Stopped. Ready to start again."); return 0; }
|
||||
if (probe != 1 && !c4fReached) { snprintf(message, size, "The app cannot reach Control4Free (%s). Stop it on its controller page.", c4fProblem); return -1; }
|
||||
if (probe != 1) { snprintf(message, size, "This copy does not answer the app (%s). Stop it on its controller page.", c4fProblem); return -1; }
|
||||
char reply[256];
|
||||
@@ -269,7 +269,7 @@ int c4fLauncherStop(char *message, size_t size)
|
||||
if (ret != 1) { snprintf(message, size, "Stop was not confirmed (%s). Check the controller page.", c4fProblem); return -1; }
|
||||
uint64_t deadline = c4fLauncherTimeMs() + 5000;
|
||||
while (c4fLauncherTimeMs() < deadline) {
|
||||
if (c4fLauncherProbe(&status) == 0) { snprintf(message, size, "Stopped. Press Cross to start it again."); return 0; }
|
||||
if (c4fLauncherProbe(&status) == 0) { snprintf(message, size, "Stopped. Ready to start again."); return 0; }
|
||||
usleep(100000);
|
||||
}
|
||||
snprintf(message, size, "Still stopping. Wait a moment, then try again."); return -1;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/* Host-only page fixtures use the real diagnostic renderer, without native
|
||||
* startup, filesystem access or report writes. Unused paths are discarded by
|
||||
* the art tool's section garbage collection. */
|
||||
#include "../launcher/diag.c"
|
||||
|
||||
void c4fDiagPreviewPage(int page, const char *text)
|
||||
{
|
||||
if (page < 0 || page >= C4F_DIAG_PAGES) return;
|
||||
char *copy = strdup(text ? text : "");
|
||||
if (!copy) return;
|
||||
pthread_mutex_lock(&c4fLock);
|
||||
free(c4fPages[page]);
|
||||
c4fPages[page] = copy;
|
||||
c4fGeneration++;
|
||||
pthread_mutex_unlock(&c4fLock);
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
/* Launcher input and navigation using the real modules and simulated native
|
||||
* readers. No controller service, network or console is contacted. */
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "autorun.h"
|
||||
#include "input.h"
|
||||
#include "navigation.h"
|
||||
#include "screen.h"
|
||||
|
||||
typedef struct {
|
||||
int handle, owned, openError, readError, connected;
|
||||
int opens, reads, closes;
|
||||
uint32_t buttons;
|
||||
} FakeSource;
|
||||
|
||||
typedef struct {
|
||||
int initialResult, initialCalls;
|
||||
int32_t user;
|
||||
FakeSource source[2];
|
||||
} FakeInput;
|
||||
|
||||
static int fakeInitial(void *context, int32_t *user)
|
||||
{
|
||||
FakeInput *fake = context;
|
||||
fake->initialCalls++;
|
||||
*user = fake->user;
|
||||
return fake->initialResult;
|
||||
}
|
||||
|
||||
static int fakeOpen(void *context, int32_t user, int port, int *owned)
|
||||
{
|
||||
FakeInput *fake = context;
|
||||
int index;
|
||||
if (port == C4F_INPUT_PORT_STANDARD) {
|
||||
assert(user == fake->user && user >= 0);
|
||||
index = C4F_INPUT_STANDARD;
|
||||
} else {
|
||||
assert(port == C4F_INPUT_PORT_REMOTE && user == C4F_INPUT_SYSTEM_USER);
|
||||
index = C4F_INPUT_REMOTE;
|
||||
}
|
||||
FakeSource *source = &fake->source[index];
|
||||
source->opens++;
|
||||
*owned = source->owned;
|
||||
return source->openError ? -1 : source->handle;
|
||||
}
|
||||
|
||||
static FakeSource *fakeHandle(FakeInput *fake, int handle)
|
||||
{
|
||||
for (int i = 0; i < 2; i++) if (fake->source[i].handle == handle) return &fake->source[i];
|
||||
assert(!"unknown handle");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int fakeRead(void *context, int handle, uint32_t *buttons, int *connected)
|
||||
{
|
||||
FakeSource *source = fakeHandle(context, handle);
|
||||
source->reads++;
|
||||
/* Poison even a failed sample: a failed native call must never leak it. */
|
||||
*buttons = source->buttons;
|
||||
*connected = source->connected;
|
||||
return source->readError ? -1 : 0;
|
||||
}
|
||||
|
||||
static void fakeClose(void *context, int handle)
|
||||
{
|
||||
FakeSource *source = fakeHandle(context, handle);
|
||||
assert(source->owned);
|
||||
source->closes++;
|
||||
assert(source->closes <= source->opens);
|
||||
}
|
||||
|
||||
static void reader(FakeInput *fake, C4fInput *input)
|
||||
{
|
||||
memset(fake, 0, sizeof(*fake));
|
||||
fake->user = 42;
|
||||
fake->source[C4F_INPUT_STANDARD] = (FakeSource){ .handle = 11, .owned = 1, .connected = 1 };
|
||||
fake->source[C4F_INPUT_REMOTE] = (FakeSource){ .handle = 22, .owned = 1, .connected = 1 };
|
||||
C4fInputOps ops = { fake, fakeInitial, fakeOpen, fakeRead, fakeClose };
|
||||
c4fInputInit(input, &ops);
|
||||
}
|
||||
|
||||
static C4fInputFrame poll(C4fInput *input, uint64_t now)
|
||||
{
|
||||
C4fInputFrame frame;
|
||||
/* Poll must fill both sources even if one cannot be opened. */
|
||||
memset(&frame, 0xff, sizeof(frame));
|
||||
c4fInputPoll(input, now, &frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
static void remoteKeyNormalization(void)
|
||||
{
|
||||
/* These are Sony remote packet key codes, not HDMI-CEC wire opcodes.
|
||||
* The native wrapper's remote-port gate is reviewed separately in main;
|
||||
* this host test checks only the helper and its downstream reader contract. */
|
||||
const uint32_t raw = UINT32_C(0x25a500f0);
|
||||
assert(c4fInputRemoteButtons(raw, 13) == (raw | C4F_INPUT_BUTTON_CROSS));
|
||||
assert(c4fInputRemoteButtons(raw, 15) == (raw | C4F_INPUT_BUTTON_CIRCLE));
|
||||
assert(c4fInputRemoteButtons(raw, 18) == (raw | C4F_INPUT_BUTTON_OPTIONS));
|
||||
assert(c4fInputRemoteButtons(C4F_INPUT_BUTTON_CROSS, 13) == C4F_INPUT_BUTTON_CROSS);
|
||||
for (unsigned code = 0; code <= UINT8_MAX; code++) {
|
||||
if (code == 13 || code == 15 || code == 18) continue;
|
||||
assert(c4fInputRemoteButtons(raw, (uint8_t)code) == raw);
|
||||
assert(c4fInputRemoteButtons(0, (uint8_t)code) == 0);
|
||||
}
|
||||
assert(c4fInputRemoteButtons(C4F_INPUT_BUTTON_INTERCEPTED, 13) == C4F_INPUT_BUTTON_INTERCEPTED);
|
||||
assert(c4fInputRemoteButtons(C4F_INPUT_BUTTON_INTERCEPTED, 15) == C4F_INPUT_BUTTON_INTERCEPTED);
|
||||
assert(c4fInputRemoteButtons(C4F_INPUT_BUTTON_INTERCEPTED, 18) == C4F_INPUT_BUTTON_INTERCEPTED);
|
||||
puts("PASS Sony remote key normalization preserves raw buttons, ignores unknown codes and respects interception");
|
||||
}
|
||||
|
||||
static void remoteOnly(void)
|
||||
{
|
||||
FakeInput fake;
|
||||
C4fInput input;
|
||||
reader(&fake, &input);
|
||||
fake.source[C4F_INPUT_STANDARD].openError = 1;
|
||||
fake.source[C4F_INPUT_REMOTE].buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
C4fInputFrame frame = poll(&input, 0);
|
||||
assert(!frame.standard.available && !frame.standard.buttons && !frame.standard.pressed);
|
||||
assert(frame.remote.available && !frame.remote.pressed);
|
||||
fake.source[C4F_INPUT_REMOTE].buttons = 0;
|
||||
poll(&input, 10);
|
||||
fake.source[C4F_INPUT_REMOTE].buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
frame = poll(&input, 20);
|
||||
assert(frame.remote.pressed == C4F_INPUT_BUTTON_CROSS);
|
||||
assert(!poll(&input, 30).remote.pressed);
|
||||
assert(fake.source[C4F_INPUT_STANDARD].opens == 1);
|
||||
c4fInputClose(&input);
|
||||
assert(!fake.source[C4F_INPUT_STANDARD].closes && fake.source[C4F_INPUT_REMOTE].closes == 1);
|
||||
c4fInputClose(&input);
|
||||
assert(fake.source[C4F_INPUT_REMOTE].closes == 1);
|
||||
puts("PASS remote-only launch, held OK baseline, no repeated activation and owned-handle cleanup");
|
||||
}
|
||||
|
||||
static void sourceEdges(void)
|
||||
{
|
||||
FakeInput fake;
|
||||
C4fInput input;
|
||||
reader(&fake, &input);
|
||||
C4fInputFrame frame = poll(&input, 0);
|
||||
assert(frame.standard.available && frame.remote.available);
|
||||
fake.source[C4F_INPUT_STANDARD].buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
frame = poll(&input, 10);
|
||||
assert(frame.standard.pressed == C4F_INPUT_BUTTON_CROSS && !frame.remote.pressed);
|
||||
fake.source[C4F_INPUT_REMOTE].buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
frame = poll(&input, 20);
|
||||
assert(!frame.standard.pressed && frame.remote.pressed == C4F_INPUT_BUTTON_CROSS);
|
||||
fake.source[C4F_INPUT_STANDARD].buttons = 0;
|
||||
frame = poll(&input, 30);
|
||||
assert(!frame.standard.buttons && frame.remote.buttons == C4F_INPUT_BUTTON_CROSS);
|
||||
assert(!frame.standard.pressed && !frame.remote.pressed);
|
||||
fake.source[C4F_INPUT_STANDARD].buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
frame = poll(&input, 40);
|
||||
assert(frame.standard.pressed == C4F_INPUT_BUTTON_CROSS && !frame.remote.pressed);
|
||||
fake.source[C4F_INPUT_REMOTE].buttons = 0;
|
||||
poll(&input, 50);
|
||||
fake.source[C4F_INPUT_REMOTE].buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
frame = poll(&input, 60);
|
||||
assert(!frame.standard.pressed && frame.remote.pressed == C4F_INPUT_BUTTON_CROSS);
|
||||
c4fInputClose(&input);
|
||||
assert(fake.source[0].closes == 1 && fake.source[1].closes == 1);
|
||||
puts("PASS independent controller/remote edges while either source holds or releases the same button");
|
||||
}
|
||||
|
||||
static void remoteOpenRetry(void)
|
||||
{
|
||||
FakeInput fake;
|
||||
C4fInput input;
|
||||
reader(&fake, &input);
|
||||
fake.source[1].openError = 1;
|
||||
C4fInputFrame frame = poll(&input, 0);
|
||||
assert(frame.standard.available && !frame.remote.available);
|
||||
fake.source[0].buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
frame = poll(&input, 10);
|
||||
assert(frame.standard.pressed == C4F_INPUT_BUTTON_CROSS && !frame.remote.buttons);
|
||||
fake.source[1].openError = 0;
|
||||
fake.source[1].buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
poll(&input, 999);
|
||||
assert(fake.source[1].opens == 1);
|
||||
frame = poll(&input, 1000);
|
||||
assert(frame.remote.available && !frame.remote.buttons && !frame.remote.pressed);
|
||||
assert(fake.source[1].opens == 2 && !frame.standard.pressed);
|
||||
fake.source[1].buttons = 0;
|
||||
poll(&input, 1010);
|
||||
fake.source[1].buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
assert(poll(&input, 1020).remote.pressed == C4F_INPUT_BUTTON_CROSS);
|
||||
c4fInputClose(&input);
|
||||
assert(fake.source[0].closes == 1 && fake.source[1].closes == 1);
|
||||
puts("PASS unavailable remote leaves DS4 operational and retry waits for a new neutral baseline");
|
||||
}
|
||||
|
||||
static void readerRecovery(void)
|
||||
{
|
||||
FakeInput fake;
|
||||
C4fInput input;
|
||||
reader(&fake, &input);
|
||||
poll(&input, 0);
|
||||
fake.source[0].buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
fake.source[1].buttons = C4F_INPUT_BUTTON_RIGHT;
|
||||
poll(&input, 10);
|
||||
fake.source[1].readError = 1;
|
||||
C4fInputFrame frame = poll(&input, 20);
|
||||
assert(frame.standard.available && frame.standard.buttons == C4F_INPUT_BUTTON_CROSS);
|
||||
assert(!frame.remote.available && !frame.remote.buttons && !frame.remote.pressed);
|
||||
assert(fake.source[1].closes == 1);
|
||||
fake.source[1].readError = 0;
|
||||
poll(&input, 1019);
|
||||
assert(fake.source[1].opens == 1);
|
||||
frame = poll(&input, 1020);
|
||||
assert(fake.source[1].opens == 2 && frame.remote.available && !frame.remote.pressed);
|
||||
fake.source[1].buttons = 0;
|
||||
poll(&input, 1030);
|
||||
fake.source[1].buttons = C4F_INPUT_BUTTON_RIGHT;
|
||||
assert(poll(&input, 1040).remote.pressed == C4F_INPUT_BUTTON_RIGHT);
|
||||
fake.source[0].connected = 0;
|
||||
frame = poll(&input, 1050);
|
||||
assert(!frame.standard.available && !frame.standard.buttons && !frame.standard.pressed);
|
||||
assert(frame.remote.available && frame.remote.buttons == C4F_INPUT_BUTTON_RIGHT);
|
||||
assert(fake.source[0].closes == 1);
|
||||
fake.source[0].connected = 1;
|
||||
poll(&input, 2049);
|
||||
assert(fake.source[0].opens == 1);
|
||||
frame = poll(&input, 2050);
|
||||
assert(frame.standard.available && !frame.standard.pressed && fake.source[0].opens == 2);
|
||||
fake.source[0].buttons = 0;
|
||||
poll(&input, 2060);
|
||||
fake.source[0].buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
assert(poll(&input, 2070).standard.pressed == C4F_INPUT_BUTTON_CROSS);
|
||||
c4fInputReset(&input, 2080);
|
||||
frame = poll(&input, 2080);
|
||||
assert(!frame.standard.pressed && !frame.remote.pressed);
|
||||
fake.source[0].buttons = fake.source[1].buttons = 0;
|
||||
poll(&input, 4000);
|
||||
fake.source[0].buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
assert(poll(&input, 4010).standard.pressed == C4F_INPUT_BUTTON_CROSS);
|
||||
c4fInputClose(&input);
|
||||
assert(fake.source[0].closes == fake.source[0].opens);
|
||||
assert(fake.source[1].closes == fake.source[1].opens);
|
||||
puts("PASS read failures/disconnects clear stale activity, bounded retry and neutral-gated recovery/reset");
|
||||
}
|
||||
|
||||
static void unavailableUserAndBorrowedHandle(void)
|
||||
{
|
||||
FakeInput fake;
|
||||
C4fInput input;
|
||||
reader(&fake, &input);
|
||||
fake.initialResult = -1;
|
||||
C4fInputFrame frame = poll(&input, 0);
|
||||
assert(!frame.standard.available && frame.remote.available);
|
||||
assert(fake.initialCalls == 1 && !fake.source[0].opens);
|
||||
fake.initialResult = 0;
|
||||
fake.user = -1;
|
||||
poll(&input, 1000);
|
||||
assert(fake.initialCalls == 2 && !fake.source[0].opens);
|
||||
fake.user = 42;
|
||||
fake.source[0].owned = 0; /* get-handle result owned by another subsystem */
|
||||
frame = poll(&input, 2000);
|
||||
assert(frame.standard.available && fake.source[0].opens == 1);
|
||||
fake.source[0].readError = 1;
|
||||
frame = poll(&input, 2010);
|
||||
assert(!frame.standard.available && !frame.standard.buttons);
|
||||
assert(!fake.source[0].closes && frame.remote.available);
|
||||
fake.source[0].readError = 0;
|
||||
frame = poll(&input, 3010);
|
||||
assert(frame.standard.available && fake.source[0].opens == 2);
|
||||
c4fInputClose(&input);
|
||||
assert(!fake.source[0].closes && fake.source[1].closes == 1);
|
||||
puts("PASS unavailable/invalid starting user does not block remote, borrowed handles remain open");
|
||||
}
|
||||
|
||||
static C4fLauncherScreen stopped(void)
|
||||
{
|
||||
C4fLauncherScreen screen;
|
||||
memset(&screen, 0, sizeof(screen));
|
||||
screen.autorun = C4F_AUTORUN_ON;
|
||||
return screen;
|
||||
}
|
||||
|
||||
static int navigate(C4fLauncherScreen *screen, uint32_t standard, uint32_t remote, int diagnostic)
|
||||
{
|
||||
uint32_t standardUnhandled = 0, remoteUnhandled = 0;
|
||||
return c4fNavigate(screen, standard, remote, diagnostic, &standardUnhandled, &remoteUnhandled);
|
||||
}
|
||||
|
||||
static int remoteFrame(FakeInput *fake, C4fInput *input, C4fLauncherScreen *screen,
|
||||
uint64_t now, uint32_t buttons)
|
||||
{
|
||||
fake->source[1].buttons = buttons;
|
||||
C4fInputFrame frame = poll(input, now);
|
||||
return navigate(screen, frame.standard.pressed, frame.remote.pressed, 0);
|
||||
}
|
||||
|
||||
static int remoteKeyFrame(FakeInput *fake, C4fInput *input, C4fLauncherScreen *screen,
|
||||
uint64_t now, uint8_t keyCode, int diagnostic)
|
||||
{
|
||||
/* The native reader supplies this normalized snapshot only for the remote
|
||||
* port. Keep the fixture's standard controller snapshots unchanged. */
|
||||
fake->source[C4F_INPUT_REMOTE].buttons = c4fInputRemoteButtons(0, keyCode);
|
||||
C4fInputFrame frame = poll(input, now);
|
||||
return navigate(screen, frame.standard.pressed, frame.remote.pressed, diagnostic);
|
||||
}
|
||||
|
||||
static void extendedRemoteNavigation(int diagnostic)
|
||||
{
|
||||
FakeInput fake;
|
||||
C4fInput input;
|
||||
reader(&fake, &input);
|
||||
fake.source[C4F_INPUT_STANDARD].openError = 1;
|
||||
C4fLauncherScreen screen = stopped();
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 0, 13, diagnostic) == C4F_ACTION_NONE);
|
||||
assert(!screen.actionMenu);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 10, 13, diagnostic) == C4F_ACTION_NONE);
|
||||
assert(!screen.actionMenu);
|
||||
remoteKeyFrame(&fake, &input, &screen, 20, 0, diagnostic);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 30, 13, diagnostic) == C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_START);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 40, 13, diagnostic) == C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu);
|
||||
remoteKeyFrame(&fake, &input, &screen, 50, 0, diagnostic);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 60, 13, diagnostic) == C4F_ACTION_START);
|
||||
assert(!screen.actionMenu);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 70, 13, diagnostic) == C4F_ACTION_NONE);
|
||||
remoteKeyFrame(&fake, &input, &screen, 80, 0, diagnostic);
|
||||
remoteKeyFrame(&fake, &input, &screen, 90, 13, diagnostic);
|
||||
remoteKeyFrame(&fake, &input, &screen, 100, 0, diagnostic);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 110, 15, diagnostic) == C4F_ACTION_RESUME);
|
||||
assert(!screen.actionMenu);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 120, 15, diagnostic) == C4F_ACTION_NONE);
|
||||
remoteKeyFrame(&fake, &input, &screen, 130, 0, diagnostic);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 140, 18, diagnostic) == C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 150, 18, diagnostic) == C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu);
|
||||
remoteKeyFrame(&fake, &input, &screen, 160, 0, diagnostic);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 170, 15, diagnostic) == C4F_ACTION_RESUME);
|
||||
|
||||
fake.source[C4F_INPUT_REMOTE].connected = 0;
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 180, 13, diagnostic) == C4F_ACTION_NONE);
|
||||
assert(!screen.actionMenu && fake.source[C4F_INPUT_REMOTE].closes == 1);
|
||||
fake.source[C4F_INPUT_REMOTE].connected = 1;
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 1179, 13, diagnostic) == C4F_ACTION_NONE);
|
||||
assert(fake.source[C4F_INPUT_REMOTE].opens == 1);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 1180, 13, diagnostic) == C4F_ACTION_NONE);
|
||||
assert(fake.source[C4F_INPUT_REMOTE].opens == 2 && !screen.actionMenu);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 1181, 13, diagnostic) == C4F_ACTION_NONE);
|
||||
remoteKeyFrame(&fake, &input, &screen, 1190, 0, diagnostic);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 1200, 13, diagnostic) == C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu);
|
||||
remoteKeyFrame(&fake, &input, &screen, 1210, 0, diagnostic);
|
||||
assert(remoteKeyFrame(&fake, &input, &screen, 1220, 15, diagnostic) == C4F_ACTION_RESUME);
|
||||
assert(!screen.actionMenu);
|
||||
c4fInputClose(&input);
|
||||
assert(fake.source[C4F_INPUT_REMOTE].closes == 2);
|
||||
puts(diagnostic ? "PASS extended remote OK/Back/Options, held-key safety and reconnect in diagnostics"
|
||||
: "PASS extended remote OK/Back/Options, held-key safety and reconnect in the launcher");
|
||||
}
|
||||
|
||||
static void heldRemoteConfirmation(void)
|
||||
{
|
||||
FakeInput fake;
|
||||
C4fInput input;
|
||||
reader(&fake, &input);
|
||||
fake.source[0].openError = 1;
|
||||
C4fLauncherScreen screen = stopped();
|
||||
screen.running = 1;
|
||||
assert(remoteFrame(&fake, &input, &screen, 0, 0) == C4F_ACTION_NONE);
|
||||
assert(remoteFrame(&fake, &input, &screen, 10, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu);
|
||||
assert(remoteFrame(&fake, &input, &screen, 20, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu);
|
||||
remoteFrame(&fake, &input, &screen, 30, 0);
|
||||
remoteFrame(&fake, &input, &screen, 40, C4F_INPUT_BUTTON_UP);
|
||||
remoteFrame(&fake, &input, &screen, 50, 0);
|
||||
remoteFrame(&fake, &input, &screen, 60, C4F_INPUT_BUTTON_UP);
|
||||
assert(screen.actionFocus == C4F_ACTION_STOP);
|
||||
remoteFrame(&fake, &input, &screen, 70, 0);
|
||||
assert(remoteFrame(&fake, &input, &screen, 80, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_NONE);
|
||||
assert(screen.confirmStop && !screen.confirmChoice);
|
||||
assert(remoteFrame(&fake, &input, &screen, 90, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_NONE);
|
||||
assert(screen.confirmStop && !screen.confirmChoice);
|
||||
remoteFrame(&fake, &input, &screen, 100, 0);
|
||||
remoteFrame(&fake, &input, &screen, 110, C4F_INPUT_BUTTON_RIGHT);
|
||||
assert(screen.confirmChoice);
|
||||
remoteFrame(&fake, &input, &screen, 120, 0);
|
||||
assert(remoteFrame(&fake, &input, &screen, 130, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_STOP);
|
||||
assert(remoteFrame(&fake, &input, &screen, 140, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_NONE);
|
||||
c4fInputClose(&input);
|
||||
puts("PASS combined native-reader/navigation flow never selects or confirms Stop from held OK");
|
||||
}
|
||||
|
||||
static void menuActions(void)
|
||||
{
|
||||
C4fLauncherScreen screen = stopped();
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_START);
|
||||
assert(navigate(&screen, 0, 0, 0) == C4F_ACTION_NONE && screen.actionMenu);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_START);
|
||||
assert(!screen.actionMenu);
|
||||
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_DOWN, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.actionFocus == C4F_ACTION_AUTORUN);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_AUTORUN);
|
||||
assert(!screen.actionMenu);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.actionFocus == C4F_ACTION_RESUME);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_RESUME);
|
||||
assert(!screen.actionMenu);
|
||||
|
||||
screen.running = 1;
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_RESUME);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.actionFocus == C4F_ACTION_CLOSE);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_CLOSE);
|
||||
assert(!screen.actionMenu);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CIRCLE, 0) == C4F_ACTION_CLOSE);
|
||||
screen = stopped();
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CIRCLE, 0) == C4F_ACTION_RESUME);
|
||||
assert(!screen.actionMenu);
|
||||
puts("PASS remote arrows/OK reach Start, Auto-start, Resume and Close; opening OK is consumed");
|
||||
}
|
||||
|
||||
static void stopConfirmation(void)
|
||||
{
|
||||
C4fLauncherScreen screen = stopped();
|
||||
screen.running = 1;
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
|
||||
assert(screen.actionFocus == C4F_ACTION_STOP);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.confirmStop && screen.confirmMenu && !screen.confirmChoice && !screen.actionMenu);
|
||||
assert(navigate(&screen, 0, 0, 0) == C4F_ACTION_NONE && screen.confirmStop);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_RESUME);
|
||||
assert(!screen.confirmStop);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_RIGHT, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.confirmChoice);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_STOP);
|
||||
assert(!screen.confirmStop);
|
||||
|
||||
/* Existing DS4 shortcuts keep their separate confirmation flow. */
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_SQUARE, 0, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.confirmStop && !screen.confirmMenu);
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_CROSS, 0, 0) == C4F_ACTION_STOP);
|
||||
assert(!screen.confirmStop);
|
||||
navigate(&screen, C4F_INPUT_BUTTON_SQUARE, 0, 0);
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_CIRCLE, 0, 0) == C4F_ACTION_RESUME);
|
||||
assert(!screen.confirmStop);
|
||||
navigate(&screen, C4F_INPUT_BUTTON_SQUARE, 0, 0);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.confirmStop && screen.confirmMenu && !screen.confirmChoice);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_RESUME);
|
||||
assert(!screen.confirmStop);
|
||||
navigate(&screen, C4F_INPUT_BUTTON_SQUARE, 0, 0);
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_CROSS, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.confirmStop && screen.confirmMenu && !screen.confirmChoice);
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_CROSS, 0, 0) == C4F_ACTION_RESUME);
|
||||
navigate(&screen, C4F_INPUT_BUTTON_SQUARE, 0, 0);
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_LEFT, 0, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.confirmStop && screen.confirmMenu && !screen.confirmChoice);
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_CROSS, 0, 0) == C4F_ACTION_RESUME);
|
||||
puts("PASS remote Stop requires a separate fresh confirmation, defaults Cancel and preserves DS4 shortcuts");
|
||||
}
|
||||
|
||||
static void unavailableActions(void)
|
||||
{
|
||||
C4fLauncherScreen screen = stopped();
|
||||
screen.autorun = C4F_AUTORUN_UNKNOWN;
|
||||
assert(!c4fActionAvailable(&screen, C4F_ACTION_AUTORUN, 0));
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_TRIANGLE, 0, 0) == C4F_ACTION_NONE);
|
||||
screen.locked = 1;
|
||||
assert(!c4fActionAvailable(&screen, C4F_ACTION_START, 0));
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_CROSS, 0, 0) == C4F_ACTION_NONE);
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_CIRCLE, 0, 0) == C4F_ACTION_CLOSE);
|
||||
screen.busy = 1;
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_CROSS | C4F_INPUT_BUTTON_TRIANGLE | C4F_INPUT_BUTTON_SQUARE,
|
||||
C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_RESUME);
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_CIRCLE, C4F_INPUT_BUTTON_CIRCLE, 0) == C4F_ACTION_RESUME);
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_CIRCLE, C4F_INPUT_BUTTON_CIRCLE, 0) == C4F_ACTION_NONE);
|
||||
screen = stopped();
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(screen.actionFocus == C4F_ACTION_START);
|
||||
screen.running = 1; /* a status refresh while the user is choosing */
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu && screen.actionFocus != C4F_ACTION_START);
|
||||
/* A direction and OK arriving together must only move the focus. */
|
||||
screen = stopped();
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_AUTORUN);
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_UP, C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_CROSS, 0)
|
||||
== C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_AUTORUN);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_UP | C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_CROSS, 0)
|
||||
== C4F_ACTION_NONE);
|
||||
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_AUTORUN);
|
||||
screen.running = 1;
|
||||
screen.actionMenu = 0;
|
||||
screen.confirmStop = screen.confirmMenu = screen.confirmChoice = 1;
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_UP, C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_CROSS, 0)
|
||||
== C4F_ACTION_NONE);
|
||||
assert(screen.confirmStop && screen.confirmChoice);
|
||||
puts("PASS unavailable/busy actions, status-change focus repair and simultaneous direction/OK safety");
|
||||
}
|
||||
|
||||
static void diagnosticNavigation(void)
|
||||
{
|
||||
C4fLauncherScreen screen = stopped();
|
||||
uint32_t standardUnhandled, remoteUnhandled;
|
||||
uint32_t directions = C4F_INPUT_BUTTON_UP | C4F_INPUT_BUTTON_RIGHT;
|
||||
assert(c4fNavigate(&screen, directions, directions, 1, &standardUnhandled, &remoteUnhandled) == C4F_ACTION_NONE);
|
||||
assert(standardUnhandled == directions && remoteUnhandled == directions);
|
||||
assert(!screen.actionMenu);
|
||||
assert(c4fNavigate(&screen, C4F_INPUT_BUTTON_OPTIONS, 0, 1,
|
||||
&standardUnhandled, &remoteUnhandled) == C4F_ACTION_NONE);
|
||||
assert(standardUnhandled == C4F_INPUT_BUTTON_OPTIONS && !remoteUnhandled);
|
||||
assert(!c4fActionAvailable(&screen, C4F_ACTION_CAPTURE, 0));
|
||||
assert(c4fActionAvailable(&screen, C4F_ACTION_CAPTURE, 1));
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 1);
|
||||
/* The action list defines display order; navigating through every row
|
||||
* ensures capture is reachable using only the TV remote. */
|
||||
C4fLauncherAction actions[8];
|
||||
int count = c4fActionList(&screen, 1, actions, 8), reached = 0;
|
||||
assert(count > 0 && count <= 8);
|
||||
for (int i = 0; i <= count; i++) {
|
||||
if (screen.actionFocus == C4F_ACTION_CAPTURE) { reached = 1; break; }
|
||||
assert(c4fNavigate(&screen, 0, C4F_INPUT_BUTTON_DOWN, 1,
|
||||
&standardUnhandled, &remoteUnhandled) == C4F_ACTION_NONE);
|
||||
assert(!remoteUnhandled);
|
||||
}
|
||||
assert(reached);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 1) == C4F_ACTION_CAPTURE);
|
||||
assert(!screen.actionMenu);
|
||||
puts("PASS diagnostic arrows remain page/scroll inputs outside Actions; remote can choose Capture");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
remoteKeyNormalization();
|
||||
remoteOnly();
|
||||
sourceEdges();
|
||||
remoteOpenRetry();
|
||||
readerRecovery();
|
||||
unavailableUserAndBorrowedHandle();
|
||||
menuActions();
|
||||
extendedRemoteNavigation(0);
|
||||
extendedRemoteNavigation(1);
|
||||
heldRemoteConfirmation();
|
||||
stopConfirmation();
|
||||
unavailableActions();
|
||||
diagnosticNavigation();
|
||||
return 0;
|
||||
}
|
||||
+16
-1
@@ -47,6 +47,14 @@ def one_reply(address, reply, check=None):
|
||||
|
||||
def main():
|
||||
web.build()
|
||||
# The launcher's own input reader and action navigation, with only the
|
||||
# native PS4 pad calls replaced. This also exercises a remote-only launch.
|
||||
for diagnostic in (False, True):
|
||||
binary = 'build/launcher-input-diag-host-test' if diagnostic else 'build/launcher-input-host-test'
|
||||
subprocess.run(['clang-18', '-std=gnu11', '-D_DEFAULT_SOURCE', '-Wall', '-Wextra', '-Werror',
|
||||
*(['-DC4F_DIAG'] if diagnostic else []), '-Ilauncher', 'launcher/input.c',
|
||||
'launcher/navigation.c', 'tests/launcher_input_test.c', '-o', binary], check=True)
|
||||
subprocess.run([binary], check=True)
|
||||
subprocess.run(['clang-18', '-std=gnu11', '-D_DEFAULT_SOURCE', '-Wall', '-Wextra', '-Werror',
|
||||
'-ffunction-sections', '-fdata-sections', '-Ilauncher', '-Itests/include',
|
||||
'tests/diag_test.c', '-Wl,--gc-sections,--wrap=write,--wrap=fsync', '-pthread',
|
||||
@@ -177,10 +185,17 @@ def main():
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
import package_launcher as package
|
||||
package.build_art_tool()
|
||||
for state in ('setup', 'running', 'stopped', 'outdated', 'mismatch', 'confirm', 'busy', 'locked', 'unknown'):
|
||||
for state in ('setup', 'running', 'stopped', 'payloader-unavailable', 'outdated', 'mismatch', 'confirm', 'busy', 'locked', 'unknown',
|
||||
'remote-actions-stopped', 'remote-actions-running', 'remote-confirm-cancel',
|
||||
'remote-confirm-stop', 'remote-actions-busy', 'remote-actions-unavailable'):
|
||||
png = ROOT / f'build/launcher-preview-{state}.png'
|
||||
package.render(['screen', state], png, 1920, 1080)
|
||||
assert png.stat().st_size > 50000, png
|
||||
package.build_art_tool(diagnostic=True)
|
||||
for state in ('remote-actions-diag', 'remote-confirm-diag', 'remote-input-diag', 'payloader-unavailable-diag'):
|
||||
png = ROOT / f'build/launcher-preview-{state}.png'
|
||||
package.render(['screen', state], png, 1920, 1080, diagnostic=True)
|
||||
assert png.stat().st_size > 50000, png
|
||||
package.render(['icon', '512'], ROOT / 'build/launcher-icon.png', 512, 512)
|
||||
assert subprocess.run([str(package.ART), 'screen', 'nonsense', '/dev/null']).returncode != 0
|
||||
# The icon the payload serves is committed, so the payload build needs no
|
||||
|
||||
+42
-6
@@ -1,13 +1,19 @@
|
||||
/* Renders launcher artwork on the build machine with the launcher's own drawing
|
||||
* code: the package icon, and screen states for review. Writes raw RGBA.
|
||||
* launcher_art icon <size> <out.rgba>
|
||||
* launcher_art screen <setup|running|stopped|outdated|confirm|busy|locked|unknown> <out.rgba> */
|
||||
* launcher_art screen <state> <out.rgba>
|
||||
* Includes TV-remote action and confirmation states. */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "autorun.h"
|
||||
#include "logo.h"
|
||||
#include "screen.h"
|
||||
#ifdef C4F_DIAG
|
||||
#include "diag.h"
|
||||
/* Host preview fixture; the native application does not link it. */
|
||||
void c4fDiagPreviewPage(int page, const char *text);
|
||||
#endif
|
||||
|
||||
static int c4fWrite(const char *path, const uint32_t *pixels, size_t count)
|
||||
{
|
||||
@@ -54,18 +60,23 @@ int main(int argc, char **argv)
|
||||
snprintf(s.address, sizeof(s.address), "http://192.168.1.20:4264");
|
||||
const char *state = argv[2], *message = "";
|
||||
s.autorun = C4F_AUTORUN_ON;
|
||||
if (!strcmp(state, "running") || !strcmp(state, "confirm") || !strcmp(state, "outdated") || !strcmp(state, "mismatch")) {
|
||||
if (!strcmp(state, "running") || !strcmp(state, "confirm") || !strcmp(state, "outdated") || !strcmp(state, "mismatch") ||
|
||||
!strcmp(state, "remote-actions-running") || !strcmp(state, "remote-confirm-cancel") ||
|
||||
!strcmp(state, "remote-confirm-stop") || !strcmp(state, "remote-confirm-diag")) {
|
||||
s.running = 1; s.controllers = 2; s.confirmStop = !strcmp(state, "confirm");
|
||||
if (!strcmp(state, "outdated")) s.autorun = C4F_AUTORUN_OUTDATED;
|
||||
if (!strcmp(state, "mismatch")) snprintf(s.runningVersion, sizeof(s.runningVersion), "older build");
|
||||
message = "Ready. Open the address on your phone or PC.";
|
||||
} else if (!strcmp(state, "setup")) {
|
||||
s.autorun = C4F_AUTORUN_OFF; message = "Not running.";
|
||||
} else if (!strcmp(state, "stopped")) {
|
||||
message = "Stopped. Press Cross to start it again.";
|
||||
} else if (!strcmp(state, "busy")) {
|
||||
} else if (!strcmp(state, "stopped") || !strcmp(state, "remote-actions-stopped") ||
|
||||
!strcmp(state, "remote-actions-diag") || !strcmp(state, "remote-input-diag")) {
|
||||
message = "Stopped. Ready to start again.";
|
||||
} else if (!strcmp(state, "payloader-unavailable") || !strcmp(state, "payloader-unavailable-diag")) {
|
||||
message = "Auto-start is on. PayLoader did not answer (errno 61). Enable PayLoader Server in GoldHEN > Server Settings, then retry Start.";
|
||||
} else if (!strcmp(state, "busy") || !strcmp(state, "remote-actions-busy")) {
|
||||
s.running = -1; s.busy = 1; s.autorun = C4F_AUTORUN_UNKNOWN; message = "Setting up Control4Free. Please wait...";
|
||||
} else if (!strcmp(state, "locked")) {
|
||||
} else if (!strcmp(state, "locked") || !strcmp(state, "remote-actions-unavailable")) {
|
||||
s.running = -1; s.locked = 1;
|
||||
message = "Could not initialize launcher. Close with PS and retry.";
|
||||
} else if (!strcmp(state, "unknown")) {
|
||||
@@ -75,9 +86,34 @@ int main(int argc, char **argv)
|
||||
} else {
|
||||
return 2;
|
||||
}
|
||||
if (!strncmp(state, "remote-actions-", 15)) {
|
||||
s.actionMenu = 1;
|
||||
s.actionFocus = !strcmp(state, "remote-actions-running") ? C4F_ACTION_STOP :
|
||||
!strcmp(state, "remote-actions-stopped") ? C4F_ACTION_START :
|
||||
!strcmp(state, "remote-actions-diag") ? C4F_ACTION_CAPTURE : C4F_ACTION_RESUME;
|
||||
} else if (!strncmp(state, "remote-confirm-", 15)) {
|
||||
s.confirmStop = s.confirmMenu = 1;
|
||||
s.confirmChoice = !strcmp(state, "remote-confirm-stop");
|
||||
}
|
||||
snprintf(s.message, sizeof(s.message), "%s", message);
|
||||
uint32_t *pixels = calloc((size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT, sizeof(uint32_t));
|
||||
if (!pixels) return 2;
|
||||
#ifdef C4F_DIAG
|
||||
if (!strcmp(state, "remote-input-diag") || !strcmp(state, "payloader-unavailable-diag")) {
|
||||
snprintf(s.inputNote, sizeof(s.inputNote),
|
||||
"DS4: disconnected (0x00000000); TV remote: ready (0x00000000), last key 0x0d / raw 0x00000000");
|
||||
char summary[768];
|
||||
snprintf(summary, sizeof(summary),
|
||||
">> Example input data for layout review.\n\nConsole\n"
|
||||
" system software: example\n\nThis app\n version %s\n"
|
||||
" input: %s\n\nService (port 4264)\n not running\n\n"
|
||||
"Auto-start\n state: on\n\nLast start from this app\n message: %s\n",
|
||||
C4F_LAUNCHER_VERSION, s.inputNote, s.message);
|
||||
c4fDiagPreviewPage(0, summary);
|
||||
}
|
||||
c4fDrawDiag(pixels, &s);
|
||||
#else
|
||||
c4fDrawLauncher(pixels, &s);
|
||||
#endif
|
||||
return c4fWrite(argv[3], pixels, (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT);
|
||||
}
|
||||
@@ -31,17 +31,30 @@ def embed_fonts():
|
||||
f'c4fFontLight={sources[0]}', f'c4fFontRegular={sources[1]}'], check=True)
|
||||
|
||||
|
||||
def build_art_tool():
|
||||
def build_art_tool(diagnostic=False):
|
||||
"""Host build of the launcher's drawing code, for the icon and previews."""
|
||||
embed_fonts()
|
||||
art_version = VERSION + ('-diag' if diagnostic else '')
|
||||
flags = ['clang-18', '-std=gnu11', '-O2', '-D_DEFAULT_SOURCE', f'-I{ROOT}/launcher',
|
||||
f'-I{ROOT}/vendor/stb', f'-I{ROOT}/vendor/qrcodegen',
|
||||
f'-DC4F_LAUNCHER_VERSION="{VERSION}"']
|
||||
f'-DC4F_LAUNCHER_VERSION="{art_version}"']
|
||||
extra_sources = []
|
||||
extra_flags = []
|
||||
art = ART
|
||||
if diagnostic:
|
||||
# Preview only: discard native startup/storage paths and retain the real
|
||||
# diagnostic drawing function. Existing host declarations provide types;
|
||||
# no console APIs or diagnostic file operations are called by this tool.
|
||||
flags += ['-DC4F_DIAG', '-ffunction-sections', '-fdata-sections', f'-I{ROOT}/tests/include']
|
||||
extra_sources = [str(ROOT / 'tests/diag_preview.c')]
|
||||
extra_flags = ['-Wl,--gc-sections', '-pthread']
|
||||
art = ART.with_name(ART.name + '-diag')
|
||||
stb = OUT / 'stb_truetype-host.o'
|
||||
subprocess.run([*flags, '-w', '-c', str(ROOT / 'launcher/stb_truetype.c'), '-o', str(stb)], check=True)
|
||||
subprocess.run([*flags, '-Wall', '-Wextra', '-Werror', str(ROOT / 'tools/launcher_art.c'),
|
||||
str(ROOT / 'launcher/screen.c'), str(ROOT / 'launcher/draw.c'), str(ROOT / 'launcher/logo.c'),
|
||||
str(ROOT / 'vendor/qrcodegen/qrcodegen.c'), str(FONTS), str(stb), '-lm', '-o', str(ART)],
|
||||
str(ROOT / 'launcher/screen.c'), str(ROOT / 'launcher/navigation.c'),
|
||||
str(ROOT / 'launcher/draw.c'), str(ROOT / 'launcher/logo.c'), *extra_sources,
|
||||
str(ROOT / 'vendor/qrcodegen/qrcodegen.c'), str(FONTS), str(stb), '-lm', *extra_flags, '-o', str(art)],
|
||||
check=True)
|
||||
|
||||
|
||||
@@ -86,10 +99,11 @@ def write_png(path, width, height, rgba, alpha=False):
|
||||
chunk(b'IDAT', zlib.compress(b''.join(rows), 9)) + chunk(b'IEND', b''))
|
||||
|
||||
|
||||
def render(args, path, width, height):
|
||||
def render(args, path, width, height, diagnostic=False):
|
||||
"""Draw with the art tool (build_art_tool first) and save a PNG."""
|
||||
raw = OUT / 'art.rgba'
|
||||
subprocess.run([str(ART), *args, str(raw)], check=True)
|
||||
raw = OUT / ('art-diag.rgba' if diagnostic else 'art.rgba')
|
||||
art = ART.with_name(ART.name + '-diag') if diagnostic else ART
|
||||
subprocess.run([str(art), *args, str(raw)], check=True)
|
||||
write_png(path, width, height, raw.read_bytes())
|
||||
raw.unlink()
|
||||
|
||||
|
||||
Reference in new issue
Block a user