4 Commits
Author SHA1 Message Date
MoHadiShibli 75743ef24b Render diagnostic previews with isolated page fixtures 2026-10-10 14:04:11 +03:00
MoHadiShibli 0b3b448ca5 Decode Sony remote Enter keys in the launcher 2026-10-10 13:58:09 +03:00
MoHadiShibli ed5e828701 Build with official mirrors and the verified OpenOrbis archive
Avoid Docker Hub pull limits using pinned Docker Official Images on ECR. Fetch the same OpenOrbis release directly and verify its SHA256; retain the existing toolchain and runtime dependencies.
2026-10-10 00:39:29 +03:00
MoHadiShibli 1242d7346e Add HDMI-CEC navigation to the PS4 launcher
Read system TV-remote input independently, expose launcher actions through arrows and OK, and preserve DS4 shortcuts. Add reconnect handling, explicit Stop confirmation, diagnostics, and reader/navigation regressions.

Prepare the next patch version as a test candidate; PS4 HDMI-CEC reception still needs hardware confirmation before release.
2026-10-10 00:19:43 +03:00
23 changed files with 1395 additions and 88 deletions

No files matched your search

+13
View File
@@ -3,10 +3,23 @@
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-10
HDMI-CEC navigation for the PS4 app.
- Added a TV-remote reader and an Actions menu to the PS4 app for HDMI-CEC navigation with
arrows and OK. Includes input recovery, separate Stop confirmation and diagnostic navigation.
Handles separate Sony remote Enter/Back/Menu key codes alongside DS4 button bits and retains
the last received key in diagnostics. The first PS4 test connected the remote but could not
select actions; the follow-up decoding still needs hardware confirmation before publication.
See the [HDMI-CEC test guide](docs/hdmi-cec.md).
## 1.2.0 — 2026-10-09
Gamepad mapping.
![DS4 Mapping with an example gamepad](https://raw.githubusercontent.com/MoHadiShibli/Control4Free/v1.2.0/docs/images/mapping.png)
- 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
+1 -1
View File
@@ -1 +1 @@
1.2.0
1.2.1
+2 -1
View File
@@ -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
+16 -4
View File
@@ -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
+59
View File
@@ -0,0 +1,59 @@
# HDMI-CEC navigation — development build
The development app includes a TV-remote input reader and an **Actions** menu.
PS4 testing is in progress; this is not part of the already-published release.
It addresses [issue #3](https://github.com/MoHadiShibli/Control4Free/issues/3), where a Samsung
TV remote navigates the PS4 home screen but stops responding inside Control4Free.
## Using the remote
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 model and connection-chain
compatibility varies; see [Sony's HDMI Device Link guide](https://manuals.playstation.net/document/en/ps4/settings/devicelink.html).
In the development app:
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.
In the 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.
The first test on firmware 11.02 with a Samsung UN55NU7090P and AA59-00818A remote showed
a connected remote reader and an open Actions menu, but OK did not select an action. The
follow-up build handles a separate Sony remote Enter/Back/Menu key field alongside ordinary
DS4 button bits. These codes are used by [PS5 SDL's remote input implementation](https://github.com/ps5-payload-dev/SDL/blob/ee4c47dc0d617b3bc8f35108f9956baf228a1322/src/video/ps5/SDL_ps5remote.c); their behavior on this
PS4 setup still needs confirmation. If OK fails again, press and release OK, then photograph
the Summary page or send its saved diagnostic report with the retained key and raw mask.
## Test before release
Use the locally built normal and diagnostic packages. Record the PS4 firmware, GoldHEN
version, TV model and any receiver/switch between the TV and console. Verify:
- App launch and arrows/OK with physical DS4s disconnected, then with a DS4 connected.
- Setup/start, auto-start on/off, updating an older auto-start copy, Stop confirmation,
Resume and Close. Holding OK must not activate an action twice.
- Diagnostic page navigation, scrolling and action selection.
- Switching the TV's HDMI input away and back, returning from the PS4 home screen, and rest/wake.
- Recovery after the remote becomes unavailable; a held button must be released before it acts again.
Complete TV-remote navigation remains unverified until these checks pass on a PS4. The app reads the system's
remote-control stream independently of DS4 input; it does not change the console's HDMI
settings, create virtual controllers, or forward TV keys to games or browser controllers.
Media/color keys and TV power/volume control are outside this implementation.
+3
View File
@@ -78,6 +78,9 @@ 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. |
TV-remote navigation is being tested in the development app. See the
[HDMI-CEC test guide](hdmi-cec.md); the published package does not yet include it.
## Updating
1. Install the new package over the old one. Delete the old app first if the installer refuses.
+1 -1
View File
@@ -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
View File
@@ -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);
+10 -4
View File
@@ -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
View File
@@ -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. */
+98
View File
@@ -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);
}
+75
View File
@@ -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
+159 -35
View File
@@ -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"
@@ -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(&params);
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);
+178
View File
@@ -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;
}
+21
View File
@@ -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
View File
@@ -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. Turn on GoldHEN's PayLoader, then choose "
"Start Control4Free in Actions or 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.",
"Choose Set up and start in Actions, or press Cross. GoldHEN will start Control4Free "
"each time it loads; it starts now if GoldHEN's PayLoader is on.",
"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 = "Choose Stop in Actions, confirm, then Start to apply this app's version. Square, Cross, then Cross also works. 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 = "Choose Update auto-start in Actions or press Triangle. 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 = "Choose Turn auto-start on in Actions or press Triangle. 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
View File
@@ -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
View File
@@ -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). Turn it on in GoldHEN, then choose Start in Actions or press Cross.", 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. Choose Start in Actions or press Cross to retry."); 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). Choose Start in Actions or press Cross to retry.", 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. Choose Start in Actions or press Cross to start it 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. Choose Start in Actions or press Cross to start it again."); return 0; }
usleep(100000);
}
snprintf(message, size, "Still stopping. Wait a moment, then try again."); return -1;
+16
View File
@@ -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);
}
+563
View File
@@ -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
View File
@@ -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', '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'):
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
+39 -6
View File
@@ -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,21 @@ 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. Choose Start in Actions or press Cross to start it again.";
} 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 +84,33 @@ 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")) {
snprintf(s.inputNote, sizeof(s.inputNote),
"DS4: disconnected (0x00000000); TV remote: ready (0x00000000), last key 0x0d / raw 0x00000000");
char summary[512];
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", C4F_LAUNCHER_VERSION, s.inputNote);
c4fDiagPreviewPage(0, summary);
}
c4fDrawDiag(pixels, &s);
#else
c4fDrawLauncher(pixels, &s);
#endif
return c4fWrite(argv[3], pixels, (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT);
}
+21 -7
View File
@@ -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()