mirror of
https://github.com/MoHadiShibli/Control4Free.git
synced 2026-10-10 16:00:48 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75743ef24b | ||
|
|
0b3b448ca5 | ||
|
|
ed5e828701 | ||
|
|
1242d7346e |
No files matched your search
@@ -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.
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||
+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,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.
|
||||
@@ -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
@@ -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);
|
||||
|
||||
+10
-4
@@ -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
|
||||
+159
-35
@@ -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(¶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. 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
@@ -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). 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;
|
||||
|
||||
@@ -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', '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
@@ -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);
|
||||
}
|
||||
@@ -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