diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..f111bab --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +buy_me_a_coffee: mortyzip diff --git a/Makefile b/Makefile index 511a11c..254f2d8 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -# Payload Manager reads the version from the filename, e.g. keepawake_ps5_v1.2.0.elf -VERSION := 1.2.0 +# Payload Manager reads the version from the filename, e.g. keepawake_ps5_v1.3.0.elf +VERSION := 1.3.0 PS5_PAYLOAD_SDK ?= $(abspath ../ps5-payload-sdk) PS4SDK ?= $(abspath ../ps4-payload-sdk) @@ -26,7 +26,7 @@ DEFINES := -DKEEPAWAKE_VERSION=\"v$(VERSION)\" -Ibuild # PS5: ELF for the ps5-payload-sdk ELF loader. PS5_CC := $(PS5_PAYLOAD_SDK)/bin/prospero-clang -PS5_CFLAGS := -Wall -Werror -O2 -lSceSystemService $(DEFINES) +PS5_CFLAGS := -Wall -Werror -O2 -lSceSystemService -lSceUserService $(DEFINES) # PS4: raw binary for GoldHEN's BinLoader, linked against libPS4. LIBPS4 := $(PS4SDK)/libPS4 @@ -64,7 +64,7 @@ $(PS4_BIN): src/ps4.c $(DEPS) host: build/keepawake_host build/keepawake_host: src/ps5.c tools/host_stubs.c $(DEPS) - cc -Wall -Werror $(DEFINES) -o $@ src/ps5.c tools/host_stubs.c + cc -Wall -Werror $(DEFINES) -DKEEPAWAKE_SETTINGS=\"build/keepawake.cfg\" -o $@ src/ps5.c tools/host_stubs.c version: @echo $(VERSION) diff --git a/README.md b/README.md index 0e79317..0f51188 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Download the payloads from the [Releases](https://github.com/mortyzip/KeepAwake/ | PS5 | `keepawake_ps5_v.elf` | ELF loader, port 9021 | | PS4 | `keepawake_ps4_v.bin` | GoldHEN BinLoader, port 9090 | -- **Start:** send the payload. The toast shows the control page address, for example "Keep Awake v enabled / Control it at http://192.168.1.50:9031". +- **Start:** send the payload. The control page opens in the console's browser, and the toast shows its address for other devices, for example "Keep Awake v enabled / Control it at http://192.168.1.50:9031". To stop the page opening on the console, switch off "Open on the PS5 at start" on the page. The setting is saved to `/data/keepawake.cfg`. - **Turn off and on:** use the control page. Turning it off lets the console go into rest mode, but the payload keeps running, so you can turn it back on from the page without sending the payload again. - **Timer:** pick 30 min, 1 hour, 2 hours or 4 hours on the page, or set your own (up to 7 days). When the time is up, Keep Awake turns itself off and shows a toast. The payload keeps running, so you can turn it back on from the page. - **Close:** use "Close Keep Awake" on the page (tap twice to confirm), or send the payload again. This ends the payload, so it needs sending again to start. @@ -29,17 +29,19 @@ While it's running, open `http://:9031` in a browser. The page shows - the time left on the timer, and when it will turn off, - quick timers (30 min, 1 hour, 2 hours, 4 hours, no limit) and a custom hours-and-minutes timer, - a button to turn it on or off, and a link to close it, -- a QR code for the page. Open the page on a computer (or the console's browser) and scan the code with your phone's camera to control Keep Awake from your phone. +- a QR code for the page. Scan it from the TV or a computer with your phone's camera to control Keep Awake from your phone, +- a switch for whether the page opens on the console when the payload starts. The page also has a small HTTP API: | Request | Does | |---|---| -| `GET /status` | Returns JSON: `version`, `console`, `active`, `uptime` and `since` (seconds since start and since it was last turned on or off), `timer` (timer length in minutes, or `null`), `remaining` (seconds left, or `null`), `interval`, `ip`, `port` | +| `GET /status` | Returns JSON: `version`, `console`, `active`, `uptime` and `since` (seconds since start and since it was last turned on or off), `timer` (timer length in minutes, or `null`), `remaining` (seconds left, or `null`), `interval`, `ip`, `port`, `open_on_start` | | `POST /on` | Turns Keep Awake on with no time limit (removing any timer), and returns the status | | `POST /on?minutes=N` | Turns Keep Awake on for N minutes (1–10080), then off. Also changes the timer if it's already on | | `POST /off` | Turns Keep Awake off without closing it, and returns the status | | `POST /quit` | Closes the payload | +| `POST /settings?open_on_start=0` or `=1` | Sets whether the page opens on the console at start, saves it, and returns the status | For example: `curl -X POST "http://:9031/on?minutes=90"`, or `make quit-ps5 PS5_HOST=`. @@ -74,7 +76,7 @@ Then open http://localhost:9031. GitHub Actions builds both payloads on every push to `main`. To publish a release: 1. Bump `VERSION` in the Makefile and commit it to `main`. -2. Tag the commit and push the tag: `git tag v1.2.1 && git push origin v1.2.1`. +2. Tag the commit and push the tag: `git tag v1.3.1 && git push origin v1.3.1`. The workflow builds the payloads and creates the GitHub Release with them attached. It fails if the tag doesn't match `VERSION`. @@ -84,3 +86,7 @@ The workflow builds the payloads and creates the GitHub Release with them attach - [src/ps4.c](src/ps4.c): built against libPS4 from the [Scene-Collective ps4-payload-sdk](https://github.com/Scene-Collective/ps4-payload-sdk). libPS4 has no `select()`, so this version checks for connections ten times a second. - [src/web.h](src/web.h): the HTTP server for the control page, shared by both. - [src/index.html](src/index.html): the control page, including a small QR code encoder (byte mode, error correction level M, up to 106 bytes). + +## Support + +Keep Awake is free. If it's useful to you, you can [buy me a coffee](https://buymeacoffee.com/mortyzip). The control page has a link too, or a QR code when it's open on the console. diff --git a/src/index.html b/src/index.html index ba47cfc..bd222d3 100644 --- a/src/index.html +++ b/src/index.html @@ -169,6 +169,58 @@ button.link.confirm { color: var(--danger); font-weight: 600; } } .custom input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } .custom button { width: auto; padding: 11px 18px; } +.setting { display: flex; align-items: center; gap: 16px; padding: 18px 20px; cursor: pointer; } +.setting div { flex: 1; } +.setting strong { display: block; font-weight: 600; } +.setting span { display: block; color: var(--muted); font-size: 14px; margin-top: 2px; } +.setting.error span { color: var(--danger); } +.switch { + flex: none; + appearance: none; + -webkit-appearance: none; + width: 46px; + height: 28px; + margin: 0; + border-radius: 14px; + background: var(--line); + position: relative; + cursor: pointer; + transition: background .15s; +} +.switch::after { + content: ""; + position: absolute; + top: 3px; + left: 3px; + width: 22px; + height: 22px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 2px rgba(0, 0, 0, .25); + transition: transform .15s; +} +.switch:checked { background: var(--accent); } +.switch:checked::after { transform: translateX(18px); } +.switch:disabled { opacity: .6; cursor: default; } +.switch:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; } +@media (prefers-reduced-motion: reduce) { .switch, .switch::after { transition: none; } } +.coffee { display: flex; align-items: center; gap: 20px; padding: 20px; } +.coffee > div { flex: 1; } +.coffee p { color: var(--muted); font-size: 14px; margin: 4px 0 12px; } +.coffee .qr { width: 112px; height: 112px; } +.coffee-button { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-radius: 12px; + background: #ffdd00; /* Buy Me a Coffee's brand yellow */ + color: #1a1a1a; + font-weight: 650; + text-decoration: none; +} +.coffee-button:hover { filter: brightness(.96); } +.coffee-button:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; } .phone { display: flex; align-items: center; gap: 20px; padding: 20px; } .phone p { color: var(--muted); font-size: 14px; margin: 4px 0 8px; } .qr { @@ -239,6 +291,27 @@ button.link.confirm { color: var(--danger); font-weight: 600; } + + +
+ +
+

Buy me a coffee

+

Keep Awake is free. If it's useful to you, you can support it with a coffee.

+ Buy me a coffee +
+
+ @@ -427,6 +500,7 @@ var qrEncode = (function () { var presets = Array.prototype.slice.call(document.querySelectorAll(".presets [data-minutes]")); var view = "loading", info = null, baseSince = 0, baseAt = 0; var pollTimer = null, quitTimer = null, busy = false, shownUrl = null, expiryPolled = false; + var settingBusy = false; function formatDuration(s) { var d = Math.floor(s / 86400), h = Math.floor(s / 3600) % 24, @@ -498,6 +572,13 @@ var qrEncode = (function () { quit.textContent = quitTimer ? "Tap again to close Keep Awake" : "Close Keep Awake"; $("note").hidden = view !== "closed" && view !== "lost"; renderQr(); + + $("setting").hidden = !live || !info; + if (info) { + setText("open-on-start-title", "Open on the " + info.console + " at start"); + if (!settingBusy) $("open-on-start").checked = info.open_on_start; + } + $("open-on-start").disabled = settingBusy; } function tick() { @@ -553,6 +634,26 @@ var qrEncode = (function () { }); custom.addEventListener("input", function () { $("custom-m").setCustomValidity(""); }); + $("open-on-start").addEventListener("change", function (e) { + var want = e.target.checked; + settingBusy = true; + render(); + request("POST", "/settings?open_on_start=" + (want ? 1 : 0)) + .then(function (data) { + settingBusy = false; + $("setting").classList.remove("error"); + setText("open-on-start-note", "Shows this page on the TV each time you send the payload"); + apply(data); + render(); + }) + .catch(function () { + settingBusy = false; + $("setting").classList.add("error"); + setText("open-on-start-note", "Couldn't save this setting. Try again."); + render(); + }); + }); + // The address phones should use: this page's own address, unless it was // opened on the console itself, where the console's LAN IP is needed. function phoneUrl() { @@ -570,12 +671,26 @@ var qrEncode = (function () { if (!url || url === shownUrl) return; shownUrl = url; - var m = qrEncode(url), n = m.length, d = ""; + $("qr").innerHTML = qrSvg(url); + setText("url", url.replace(/\/$/, "")); + } + + function qrSvg(text) { + var m = qrEncode(text), n = m.length, d = ""; for (var y = 0; y < n; y++) for (var x = 0; x < n; x++) if (m[y][x]) d += "M" + (x + 4) + " " + (y + 4) + "h1v1h-1z"; - $("qr").innerHTML = ''; - setText("url", url.replace(/\/$/, "")); + } + + // Paying from the console's browser isn't practical, so on a PlayStation + // show a QR code to open the page on a phone instead of a link. + if (/PlayStation/i.test(navigator.userAgent)) { + var coffeeQr = $("coffee-qr"); + coffeeQr.innerHTML = qrSvg($("coffee-link").href); + coffeeQr.hidden = false; + $("coffee-link").hidden = true; + setText("coffee-text", "Keep Awake is free. If it's useful to you, scan this with your phone to support it with a coffee."); } function apply(data) { diff --git a/src/ps4.c b/src/ps4.c index 8db2e1e..600145c 100644 --- a/src/ps4.c +++ b/src/ps4.c @@ -105,6 +105,29 @@ make_addr(struct sockaddr_in *addr, unsigned int ip) { } +/* Runs on its own thread, so the control page is served even if launching + the browser blocks. */ +static void* +open_browser_thread(void *arg) { + static char url[64]; + + UNUSED(arg); + snprintf(url, sizeof url, "http://127.0.0.1:%d/", KEEPAWAKE_PORT); + openBrowser(url); + return NULL; +} + + +static void +open_browser(void) { + ScePthread thread; + + if(scePthreadCreate(&thread, NULL, open_browser_thread, NULL, "keepawake_browser") == 0) { + scePthreadDetach(thread); + } +} + + /* Ask an already running instance to quit. Returns 0 if it confirmed. */ static int quit_running_instance(void) { @@ -175,6 +198,8 @@ _main(struct thread *td) { initKernel(); initLibc(); initNetwork(); + initPthread(); + initSysUtil(); jailbreak(); sceNetCtlInit(); @@ -196,6 +221,7 @@ _main(struct thread *td) { } g_started = sceKernelGetProcessTime(); + ka_load_settings(); web_get_ip(ip, sizeof ip); if(ip[0]) { @@ -206,6 +232,10 @@ _main(struct thread *td) { "Control page on port %d", KEEPAWAKE_PORT); } + if(ka_open_on_start) { + open_browser(); + } + while(1) { ka_tick(); diff --git a/src/ps5.c b/src/ps5.c index a1edbd6..56ceead 100644 --- a/src/ps5.c +++ b/src/ps5.c @@ -3,7 +3,8 @@ The console's idle timer is reset by periodically calling sceSystemServicePowerTick(). The payload also serves a control page on TCP port 9031 (see web.h), which - - shows the status and turns Keep Awake on and off from a browser, + - shows the status and turns Keep Awake on and off from a browser + (and opens in the PS5's browser at start, unless turned off), - guarantees only one instance runs at a time, and - lets a second copy of the payload close the running one. */ @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +41,8 @@ typedef struct notify_request { int sceKernelSendNotificationRequest(int, notify_request_t*, size_t, int); int sceSystemServicePowerTick(void); +int sceSystemServiceLaunchWebBrowser(const char *uri, void *param); +int sceUserServiceInitialize(void *param); static time_t g_started; @@ -129,6 +133,31 @@ web_notify(const char *msg) { } +/* Runs on its own thread, so the control page is served even if launching + the browser blocks. */ +static void* +open_browser_thread(void *arg) { + char url[64]; + + snprintf(url, sizeof url, "http://127.0.0.1:%d/", KEEPAWAKE_PORT); + sceUserServiceInitialize(0); + if(sceSystemServiceLaunchWebBrowser(url, 0) != 0) { + notify("Keep Awake couldn't open the browser"); + } + return NULL; +} + + +static void +open_browser(void) { + pthread_t thread; + + if(pthread_create(&thread, NULL, open_browser_thread, NULL) == 0) { + pthread_detach(thread); + } +} + + /* Ask an already running instance to quit. Returns 0 if it confirmed. */ static int quit_running_instance(void) { @@ -221,6 +250,7 @@ main(void) { } g_started = now_seconds(); + ka_load_settings(); web_get_ip(ip, sizeof ip); if(ip[0]) { @@ -231,6 +261,10 @@ main(void) { "Control page on port %d", KEEPAWAKE_PORT); } + if(ka_open_on_start) { + open_browser(); + } + while(1) { FD_ZERO(&fds); FD_SET(srv, &fds); diff --git a/src/web.h b/src/web.h index 9c014d0..5bbf823 100644 --- a/src/web.h +++ b/src/web.h @@ -13,6 +13,10 @@ POST /on?minutes=N keep it awake for N minutes, then turn off POST /off stop keeping it awake, but stay running POST /quit end the payload + POST /settings?open_on_start=0|1 + whether to open this page in the console's + browser when the payload starts (saved to + KEEPAWAKE_SETTINGS) The platform source defines WEB_CONSOLE ("PS5" or "PS4") before including this file, and implements the functions declared below. Client sockets are @@ -48,12 +52,54 @@ static void web_notify(const char *msg); #define KA_MAX_MINUTES (7 * 24 * 60) +#ifndef KEEPAWAKE_SETTINGS +#define KEEPAWAKE_SETTINGS "/data/keepawake.cfg" +#endif + static int ka_active = 1; // resetting the idle timer? static long long ka_changed = 0; // web_uptime() when on/off last changed static long long ka_next_tick = 0; // web_uptime() when the next tick is due static long long ka_until = 0; // web_uptime() when the timer ends, 0 if none static long long ka_minutes = 0; // length of the current timer +static int ka_open_on_start = 1; // open the page in the console's browser at start + + +static void +ka_load_settings(void) { + char buf[128]; + int fd; + int n; + + if((fd = open(KEEPAWAKE_SETTINGS, O_RDONLY, 0)) < 0) { + return; // no file yet: keep the defaults + } + n = read(fd, buf, sizeof buf - 1); + close(fd); + + if(n > 0) { + buf[n] = 0; + ka_open_on_start = !strstr(buf, "open_on_start=0"); + } +} + + +static int +ka_save_settings(void) { + char buf[64]; + int fd; + int n; + int ok; + + n = snprintf(buf, sizeof buf, "open_on_start=%d\n", ka_open_on_start); + if((fd = open(KEEPAWAKE_SETTINGS, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) { + return -1; + } + ok = write(fd, buf, n) == n; + close(fd); + + return ok ? 0 : -1; +} /* "1 hour 30 minutes", "2 hours", "45 minutes" */ @@ -243,10 +289,10 @@ web_respond_status(int fd) { snprintf(json, sizeof json, "{\"version\":\"%s\",\"console\":\"%s\",\"active\":%s," "\"uptime\":%lld,\"since\":%lld,\"timer\":%s,\"remaining\":%s," - "\"interval\":%d,\"ip\":\"%s\",\"port\":%d}", + "\"interval\":%d,\"ip\":\"%s\",\"port\":%d,\"open_on_start\":%s}", KEEPAWAKE_VERSION, WEB_CONSOLE, ka_active ? "true" : "false", now, now - ka_changed, timer, remaining, KEEPAWAKE_TICK_SECONDS, - ip, KEEPAWAKE_PORT); + ip, KEEPAWAKE_PORT, ka_open_on_start ? "true" : "false"); web_respond_json(fd, "200 OK", json); } @@ -321,9 +367,26 @@ web_handle_client(int fd) { web_respond_json(fd, "200 OK", "{\"quit\":true}"); return 1; + } else if(web_route(req, len, "POST", "/settings")) { + long long open_on_start; + rc = web_query_number(req, len, "open_on_start", &open_on_start); + if(rc <= 0 || open_on_start > 1) { + web_respond_json(fd, "400 Bad Request", + "{\"error\":\"open_on_start must be 0 or 1\"}"); + } else { + ka_open_on_start = (int)open_on_start; + if(ka_save_settings() != 0) { + web_respond_json(fd, "500 Internal Server Error", + "{\"error\":\"couldn't save the setting\"}"); + } else { + web_respond_status(fd); + } + } + } else if(web_route(req, len, "GET", "/on") || web_route(req, len, "GET", "/off") || - web_route(req, len, "GET", "/quit")) { + web_route(req, len, "GET", "/quit") || + web_route(req, len, "GET", "/settings")) { // Only POST changes anything, so link previews and prefetching can't. web_respond_json(fd, "405 Method Not Allowed", "{\"error\":\"use POST\"}"); diff --git a/tools/host_stubs.c b/tools/host_stubs.c index a2570ce..4fd23e2 100644 --- a/tools/host_stubs.c +++ b/tools/host_stubs.c @@ -17,3 +17,17 @@ sceSystemServicePowerTick(void) { fflush(stdout); return 0; } + + +int +sceUserServiceInitialize(void *param) { + return 0; +} + + +int +sceSystemServiceLaunchWebBrowser(const char *uri, void *param) { + printf("[KeepAwake] open browser: %s\n", uri); + fflush(stdout); + return 0; +}