Add control page with on/off, QR code and HTTP API (v1.1.0)

This commit is contained in:
mortyzip committed 2026-10-07 13:53:30 +01:00
1 parent d4ac76be97
commit 332ccf4859
9 files changed
+1097 -74

No files matched your search

+2 -2
View File
@@ -18,10 +18,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install LLVM
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y clang-18 lld-18 llvm-18
sudo apt-get install -y clang-18 lld-18 llvm-18 xxd
echo "LLVM_CONFIG=/usr/lib/llvm-18/bin/llvm-config" >> "$GITHUB_ENV"
- name: Download SDKs
+1
View File
@@ -1,2 +1,3 @@
*.o
.DS_Store
build/
+33 -19
View File
@@ -1,5 +1,5 @@
# Payload Manager reads the version from the filename, e.g. keepawake_ps5_v1.0.0.elf
VERSION := 1.0.0
# Payload Manager reads the version from the filename, e.g. keepawake_ps5_v1.1.0.elf
VERSION := 1.1.0
PS5_PAYLOAD_SDK ?= $(abspath ../ps5-payload-sdk)
PS4SDK ?= $(abspath ../ps4-payload-sdk)
@@ -19,7 +19,10 @@ LLVM_PFX := $(if $(LLVM_BIN),$(LLVM_BIN)/)
PS5_ELF := keepawake_ps5_v$(VERSION).elf
PS4_BIN := keepawake_ps4_v$(VERSION).bin
DEFINES := -DKEEPAWAKE_VERSION=\"v$(VERSION)\"
# The control page is embedded into the payloads as a C array.
PAGE_H := build/index_html.h
DEPS := src/web.h $(PAGE_H)
DEFINES := -DKEEPAWAKE_VERSION=\"v$(VERSION)\" -Ibuild
# PS5: ELF for the ps5-payload-sdk ELF loader.
PS5_CC := $(PS5_PAYLOAD_SDK)/bin/prospero-clang
@@ -42,32 +45,43 @@ all: ps5 ps4
ps5: $(PS5_ELF)
ps4: $(PS4_BIN)
$(PS5_ELF): src/ps5.c
$(PS5_CC) $(PS5_CFLAGS) -o $@ $^
$(PAGE_H): src/index.html
@mkdir -p build
cd src && xxd -i index.html > ../$@
$(PS4_BIN): src/ps4.c
$(PS4_CC) $(PS4_CFLAGS) -c -o ps4.o src/ps4.c
$(PS4_CC) --target=x86_64-unknown-freebsd -c -o ps4_crt0.o $(LIBPS4)/crt0.s
$(PS4_LD) -o ps4.elf ps4_crt0.o ps4.o $(PS4_LDFLAGS)
$(PS4_OBJCOPY) -O binary ps4.elf $@
rm -f ps4.o ps4_crt0.o ps4.elf
$(PS5_ELF): src/ps5.c $(DEPS)
$(PS5_CC) $(PS5_CFLAGS) -o $@ src/ps5.c
$(PS4_BIN): src/ps4.c $(DEPS)
@mkdir -p build
$(PS4_CC) $(PS4_CFLAGS) -c -o build/ps4.o src/ps4.c
$(PS4_CC) --target=x86_64-unknown-freebsd -c -o build/ps4_crt0.o $(LIBPS4)/crt0.s
$(PS4_LD) -o build/ps4.elf build/ps4_crt0.o build/ps4.o $(PS4_LDFLAGS)
$(PS4_OBJCOPY) -O binary build/ps4.elf $@
# Runs the PS5 code on this computer with the console calls stubbed out, for
# working on the control page: make host && ./build/keepawake_host
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
version:
@echo $(VERSION)
clean:
rm -f keepawake*.elf keepawake*.bin ps4.o ps4_crt0.o ps4.elf
rm -rf build keepawake*.elf keepawake*.bin
test-ps5: $(PS5_ELF)
$(PS5_PAYLOAD_SDK)/bin/prospero-deploy -h $(PS5_HOST) -p $(PS5_PORT) $^
$(PS5_PAYLOAD_SDK)/bin/prospero-deploy -h $(PS5_HOST) -p $(PS5_PORT) $<
test-ps4: $(PS4_BIN)
nc -w 1 $(PS4_HOST) $(PS4_PORT) < $^
nc -w 1 $(PS4_HOST) $(PS4_PORT) < $<
stop-ps5:
nc -z $(PS5_HOST) 9031
quit-ps5:
curl -fsS -X POST http://$(PS5_HOST):9031/quit
stop-ps4:
nc -z $(PS4_HOST) 9031
quit-ps4:
curl -fsS -X POST http://$(PS4_HOST):9031/quit
.PHONY: all ps5 ps4 version clean test-ps5 test-ps4 stop-ps5 stop-ps4
.PHONY: all ps5 ps4 host version clean test-ps5 test-ps4 quit-ps5 quit-ps4
+38 -5
View File
@@ -13,11 +13,32 @@ Download the payloads from the [Releases](https://github.com/mortyzip/KeepAwake/
| PS5 | `keepawake_ps5_v<version>.elf` | ELF loader, port 9021 |
| PS4 | `keepawake_ps4_v<version>.bin` | GoldHEN BinLoader, port 9090 |
- **Enable:** send the payload. You'll see "Keep Awake v<version> enabled".
- **Disable:** send the payload again. You'll see "Keep Awake v<version> disabled". You can also connect to port 9031 on the console, for example with `make stop-ps5` or `nc -z <console-ip> 9031`.
- **Start:** send the payload. The toast shows the control page address, for example "Keep Awake v<version> enabled / Control it at http://192.168.1.50:9031".
- **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.
- **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.
Only one copy runs at a time. It uses TCP port 9031 to know whether it's already running.
### Control page
While it's running, open `http://<console-ip>:9031` in a browser. The page shows:
- whether Keep Awake is on or off, and for how long,
- which console it's on,
- 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.
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), `interval`, `ip`, `port` |
| `POST /on` | Turns Keep Awake on, and returns the status |
| `POST /off` | Turns Keep Awake off without closing it, and returns the status |
| `POST /quit` | Closes the payload |
For example: `curl -X POST http://<console-ip>:9031/off`, or `make quit-ps5 PS5_HOST=<ps5-ip>`.
## Building
The Makefile expects the SDKs next to this folder (`../ps5-payload-sdk` and `../ps4-payload-sdk`). Set `PS5_PAYLOAD_SDK` or `PS4SDK` to use them from somewhere else.
@@ -32,18 +53,30 @@ make test-ps4 PS4_HOST=<ps4-ip>
Both builds need LLVM (`clang`, `ld.lld`, `llvm-objcopy`). The Makefile uses Homebrew's LLVM automatically when it's installed. Otherwise, set `LLVM_CONFIG` to your `llvm-config`.
The version is set by `VERSION` in the [Makefile](Makefile). It goes into the filenames, where Payload Manager reads it, and into the toasts.
The version is set by `VERSION` in the [Makefile](Makefile). It goes into the filenames, where Payload Manager reads it, the toasts, and the control page.
### Working on the control page
The page is [src/index.html](src/index.html). The build embeds it into both payloads with `xxd`. To try it without a console, run the PS5 code on your computer with the console calls stubbed out:
```sh
make host && ./build/keepawake_host
```
Then open http://localhost:9031.
## Releasing
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.0.1 && git push origin v1.0.1`.
2. Tag the commit and push the tag: `git tag v1.1.1 && git push origin v1.1.1`.
The workflow builds the payloads and creates the GitHub Release with them attached. It fails if the tag doesn't match `VERSION`.
## Source
- [src/ps5.c](src/ps5.c): built with the [ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk).
- [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 a stop request once a second.
- [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).
+518
View File
@@ -0,0 +1,518 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Keep Awake</title>
<style>
:root {
--bg: #f4f5f7;
--card: #ffffff;
--text: #16181d;
--muted: #5d6470;
--line: #e3e6eb;
--accent: #0f9d76;
--accent-ink: #ffffff;
--accent-soft: rgba(15, 157, 118, .14);
--danger: #c93a3a;
--danger-soft: rgba(201, 58, 58, .1);
--off: #9aa1ab;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0e1014;
--card: #171a20;
--text: #eef0f3;
--muted: #9aa1ad;
--line: #262a32;
--accent: #2fd3a0;
--accent-ink: #062a1f;
--accent-soft: rgba(47, 211, 160, .14);
--danger: #ff6b6b;
--danger-soft: rgba(255, 107, 107, .12);
--off: #5f6672;
}
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
min-height: 100vh;
background: var(--bg);
color: var(--text);
font: 16px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
display: flex;
justify-content: center;
padding: 40px 16px;
}
main { width: 100%; max-width: 420px; }
header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
h1 { font-size: 20px; font-weight: 650; margin: 0; letter-spacing: -.01em; }
h2 { font-size: 16px; font-weight: 650; margin: 0; }
.chip {
font-size: 13px;
color: var(--muted);
border: 1px solid var(--line);
border-radius: 999px;
padding: 2px 10px;
}
.card {
background: var(--card);
border: 1px solid var(--line);
border-radius: 18px;
padding: 28px 24px 24px;
}
.card + .card { margin-top: 16px; }
.status { display: flex; align-items: center; gap: 16px; }
.dot {
flex: none;
width: 52px;
height: 52px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--accent-soft);
}
.dot::after {
content: "";
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--accent);
}
.on .dot { animation: pulse 2.4s ease-in-out infinite; }
.card:not(.on) .dot { background: var(--line); }
.card:not(.on) .dot::after { background: var(--off); }
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 var(--accent-soft); }
50% { box-shadow: 0 0 0 10px transparent; }
}
@media (prefers-reduced-motion: reduce) { .on .dot { animation: none; } }
.state { font-size: 22px; font-weight: 650; margin: 0; }
.detail { color: var(--muted); margin: 2px 0 0; }
dl {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
margin: 24px 0;
background: var(--line);
border: 1px solid var(--line);
border-radius: 12px;
overflow: hidden;
}
dl div { background: var(--card); padding: 12px 14px; }
dl div.wide { grid-column: span 2; }
dt { font-size: 13px; color: var(--muted); }
dd { margin: 2px 0 0; font-weight: 600; font-variant-numeric: tabular-nums; }
.closed dl, .lost dl, .loading dl { opacity: .5; }
button {
width: 100%;
font: inherit;
font-weight: 600;
padding: 13px 16px;
border-radius: 12px;
cursor: pointer;
border: 1px solid var(--line);
color: var(--text);
background: transparent;
}
button:hover:not(:disabled) { background: var(--line); }
button.primary { border-color: var(--accent); background: var(--accent); color: var(--accent-ink); }
button.primary:hover:not(:disabled) { background: var(--accent); filter: brightness(1.06); }
button:disabled { opacity: .6; cursor: default; }
button:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
button.link {
width: auto;
display: block;
margin: 14px auto 0;
padding: 6px 10px;
border: 0;
font-size: 14px;
font-weight: 500;
color: var(--muted);
}
button.link:hover:not(:disabled) { background: none; color: var(--danger); }
button.link.confirm { color: var(--danger); font-weight: 600; }
.note { color: var(--muted); font-size: 14px; margin: 14px 2px 0; text-align: center; }
.phone { display: flex; align-items: center; gap: 20px; padding: 20px; }
.phone p { color: var(--muted); font-size: 14px; margin: 4px 0 8px; }
.qr {
flex: none;
width: 132px;
height: 132px;
background: #fff; /* always light, so scanners read it in dark mode too */
border-radius: 10px;
padding: 6px;
}
.qr svg { display: block; width: 100%; height: 100%; }
.url { font-size: 14px; font-weight: 600; word-break: break-all; }
[hidden] { display: none !important; }
</style>
</head>
<body>
<main>
<header>
<h1>Keep Awake</h1>
<span class="chip" id="version">&nbsp;</span>
</header>
<section class="card loading" id="card" aria-live="polite">
<div class="status">
<div class="dot" aria-hidden="true"></div>
<div>
<p class="state" id="state">Connecting…</p>
<p class="detail" id="detail">Checking the console</p>
</div>
</div>
<dl>
<div><dt id="since-label">On for</dt><dd id="since">–</dd></div>
<div><dt>Console</dt><dd id="console">–</dd></div>
<div class="wide"><dt>Idle timer reset</dt><dd id="interval">–</dd></div>
</dl>
<button type="button" id="toggle" disabled>Turn off</button>
<button type="button" class="link" id="quit" hidden>Close Keep Awake</button>
</section>
<section class="card phone" id="phone" hidden>
<div class="qr" id="qr" role="img" aria-label="QR code for this page"></div>
<div>
<h2>Control from your phone</h2>
<p>Scan with your phone's camera, or open:</p>
<div class="url" id="url"></div>
</div>
</section>
<p class="note" id="note" hidden>To start it again, send the payload again.</p>
</main>
<script>
/* QR-BEGIN
Minimal QR code encoder: byte mode, error correction level M,
versions 1-6 (up to 106 bytes). Follows the structure of Project Nayuki's
QR Code generator. qrEncode(text, mask?) returns a square array of rows of
booleans (true = dark). */
var qrEncode = (function () {
var ECC = [0, 10, 16, 26, 18, 24, 16]; // EC codewords per block, level M
var BLOCKS = [0, 1, 1, 1, 2, 2, 4]; // number of blocks, level M
function rawModules(v) {
var r = (16 * v + 128) * v + 64;
if (v >= 2) { var n = Math.floor(v / 7) + 2; r -= (25 * n - 10) * n - 55; }
return r;
}
function dataCodewords(v) { return Math.floor(rawModules(v) / 8) - ECC[v] * BLOCKS[v]; }
function gfMul(x, y) {
var z = 0;
for (var i = 7; i >= 0; i--) { z = (z << 1) ^ ((z >>> 7) * 0x11d); z ^= ((y >>> i) & 1) * x; }
return z;
}
function rsDivisor(deg) {
var r = []; for (var i = 0; i < deg - 1; i++) r.push(0); r.push(1);
var root = 1;
for (i = 0; i < deg; i++) {
for (var j = 0; j < r.length; j++) { r[j] = gfMul(r[j], root); if (j + 1 < r.length) r[j] ^= r[j + 1]; }
root = gfMul(root, 2);
}
return r;
}
function rsRemainder(data, div) {
var r = div.map(function () { return 0; });
data.forEach(function (b) {
var f = b ^ r.shift(); r.push(0);
div.forEach(function (c, i) { r[i] ^= gfMul(c, f); });
});
return r;
}
return function (text, forceMask) {
var bytes = Array.prototype.slice.call(new TextEncoder().encode(text));
for (var v = 1; v <= 6 && bytes.length + 2 > dataCodewords(v); v++);
if (v > 6) throw new Error("text too long for QR");
// Data bit stream: byte mode, length, data, terminator, padding.
var bits = [], cap = dataCodewords(v) * 8;
function put(val, len) { for (var i = len - 1; i >= 0; i--) bits.push((val >>> i) & 1); }
put(4, 4); put(bytes.length, 8); bytes.forEach(function (b) { put(b, 8); });
put(0, Math.min(4, cap - bits.length));
put(0, (8 - bits.length % 8) % 8);
for (var pad = 0xec; bits.length < cap; pad ^= 0xec ^ 0x11) put(pad, 8);
var data = [];
for (var i = 0; i < bits.length; i += 8) {
var b = 0; for (var k = 0; k < 8; k++) b = (b << 1) | bits[i + k]; data.push(b);
}
// Split into blocks, add error correction, interleave.
var nb = BLOCKS[v], ecl = ECC[v], raw = Math.floor(rawModules(v) / 8);
var nShort = nb - raw % nb, shortLen = Math.floor(raw / nb), div = rsDivisor(ecl);
var blocks = [];
for (i = 0, k = 0; i < nb; i++) {
var dat = data.slice(k, k + shortLen - ecl + (i < nShort ? 0 : 1));
k += dat.length;
var ecc = rsRemainder(dat, div);
if (i < nShort) dat.push(0);
blocks.push(dat.concat(ecc));
}
var words = [];
for (i = 0; i < blocks[0].length; i++) {
blocks.forEach(function (blk, j) { if (i != shortLen - ecl || j >= nShort) words.push(blk[i]); });
}
var size = v * 4 + 17, mod = [], fn = [];
for (i = 0; i < size; i++) { mod.push(new Array(size).fill(false)); fn.push(new Array(size).fill(false)); }
function setFn(x, y, d) { mod[y][x] = d; fn[y][x] = true; }
// Function patterns: timing, finders, alignment.
for (i = 0; i < size; i++) { setFn(6, i, i % 2 == 0); setFn(i, 6, i % 2 == 0); }
[[3, 3], [size - 4, 3], [3, size - 4]].forEach(function (c) {
for (var dy = -4; dy <= 4; dy++) for (var dx = -4; dx <= 4; dx++) {
var d = Math.max(Math.abs(dx), Math.abs(dy)), x = c[0] + dx, y = c[1] + dy;
if (x >= 0 && x < size && y >= 0 && y < size) setFn(x, y, d != 2 && d != 4);
}
});
if (v >= 2) {
var pos = [6, size - 7];
pos.forEach(function (ax, ai) { pos.forEach(function (ay, aj) {
if ((ai == 0 && aj == 0) || (ai == 0 && aj == 1) || (ai == 1 && aj == 0)) return;
for (var dy = -2; dy <= 2; dy++) for (var dx = -2; dx <= 2; dx++)
setFn(ax + dx, ay + dy, Math.max(Math.abs(dx), Math.abs(dy)) != 1);
}); });
}
function drawFormat(mask) {
var d = (0 << 3) | mask, r = d; // level M = 0b00
for (var i = 0; i < 10; i++) r = (r << 1) ^ ((r >>> 9) * 0x537);
var f = ((d << 10) | r) ^ 0x5412;
function bit(i) { return ((f >>> i) & 1) != 0; }
for (i = 0; i <= 5; i++) setFn(8, i, bit(i));
setFn(8, 7, bit(6)); setFn(8, 8, bit(7)); setFn(7, 8, bit(8));
for (i = 9; i < 15; i++) setFn(14 - i, 8, bit(i));
for (i = 0; i < 8; i++) setFn(size - 1 - i, 8, bit(i));
for (i = 8; i < 15; i++) setFn(8, size - 15 + i, bit(i));
setFn(8, size - 8, true);
}
drawFormat(0); // reserve the format areas before placing data
// Place data in the zigzag order.
for (var right = size - 1, n = 0; right >= 1; right -= 2) {
if (right == 6) right = 5;
for (var vert = 0; vert < size; vert++) for (var j = 0; j < 2; j++) {
var x = right - j, y = ((right + 1) & 2) == 0 ? size - 1 - vert : vert;
if (!fn[y][x] && n < words.length * 8) { mod[y][x] = ((words[n >>> 3] >>> (7 - (n & 7))) & 1) != 0; n++; }
}
}
var MASKS = [
function (x, y) { return (x + y) % 2 == 0; },
function (x, y) { return y % 2 == 0; },
function (x, y) { return x % 3 == 0; },
function (x, y) { return (x + y) % 3 == 0; },
function (x, y) { return (Math.floor(x / 3) + Math.floor(y / 2)) % 2 == 0; },
function (x, y) { return x * y % 2 + x * y % 3 == 0; },
function (x, y) { return (x * y % 2 + x * y % 3) % 2 == 0; },
function (x, y) { return ((x + y) % 2 + x * y % 3) % 2 == 0; }
];
function applyMask(m) {
for (var y = 0; y < size; y++) for (var x = 0; x < size; x++)
if (!fn[y][x] && MASKS[m](x, y)) mod[y][x] = !mod[y][x];
}
// Penalty score (runs, 2x2 blocks, finder-like patterns, balance), as in
// the QR spec, to pick the most readable mask.
function penalty() {
var score = 0, dark = 0;
function lineScore(get) {
var s = 0;
for (var a = 0; a < size; a++) {
var run = 0, prev = null, seq = [];
for (var b = 0; b < size; b++) {
var c = get(a, b);
if (c === prev) { run++; } else { if (run >= 5) s += run - 2; run = 1; prev = c; }
seq.push(c ? 1 : 0);
}
if (run >= 5) s += run - 2;
var line = seq.join("");
var re = /(?=(10111010000|00001011101))/g;
while (re.exec(line)) { s += 40; re.lastIndex++; }
}
return s;
}
score += lineScore(function (a, b) { return mod[a][b]; });
score += lineScore(function (a, b) { return mod[b][a]; });
for (var y = 0; y < size - 1; y++) for (var x = 0; x < size - 1; x++) {
var c = mod[y][x];
if (c == mod[y][x + 1] && c == mod[y + 1][x] && c == mod[y + 1][x + 1]) score += 3;
}
for (y = 0; y < size; y++) for (x = 0; x < size; x++) if (mod[y][x]) dark++;
var total = size * size;
score += Math.floor(Math.abs(dark * 20 - total * 10) / total) * 10;
return score;
}
var best = forceMask, bestScore = Infinity;
if (best === undefined) {
for (var m = 0; m < 8; m++) {
applyMask(m); drawFormat(m);
var s = penalty();
if (s < bestScore) { best = m; bestScore = s; }
applyMask(m);
}
}
applyMask(best); drawFormat(best);
return mod;
};
})();
/* QR-END */
</script>
<script>
(function () {
var $ = function (id) { return document.getElementById(id); };
var card = $("card"), toggle = $("toggle"), quit = $("quit");
var view = "loading", info = null, baseSince = 0, baseAt = 0;
var pollTimer = null, quitTimer = null, busy = false, shownUrl = null;
function formatDuration(s) {
var d = Math.floor(s / 86400), h = Math.floor(s / 3600) % 24,
m = Math.floor(s / 60) % 60, sec = s % 60;
if (d) return d + "d " + h + "h";
if (h) return h + "h " + m + "m";
if (m) return m + "m " + sec + "s";
return sec + "s";
}
function setText(id, text) { $(id).textContent = text; }
function render() {
card.className = "card " + view;
var name = info ? info.console : "console";
var texts = {
loading: ["Connecting…", "Checking the console"],
on: ["On", "Keeping your " + name + " awake"],
off: ["Off", "Your " + name + " can go into rest mode"],
closed: ["Closed", "Keep Awake has stopped running"],
lost: ["Not running", "Keep Awake isn't responding"]
}[view];
setText("state", texts[0]);
setText("detail", texts[1]);
if (info) {
setText("version", info.version);
setText("console", info.console);
setText("since-label", info.active ? "On for" : "Off for");
setText("interval", info.active ? "Every " + info.interval + " seconds" : "Paused");
}
tick();
var live = view === "on" || view === "off";
toggle.disabled = busy || view === "loading";
toggle.className = view === "off" ? "primary" : "";
toggle.textContent = view === "on" ? "Turn off" : view === "off" ? "Turn on" : "Check again";
quit.hidden = !live;
quit.disabled = busy;
quit.className = quitTimer ? "link confirm" : "link";
quit.textContent = quitTimer ? "Tap again to close Keep Awake" : "Close Keep Awake";
$("note").hidden = view !== "closed" && view !== "lost";
renderQr();
}
function tick() {
if ((view === "on" || view === "off") && info) {
setText("since", formatDuration(baseSince + Math.floor((Date.now() - baseAt) / 1000)));
}
}
// 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() {
var host = location.hostname;
if (host && host !== "localhost" && host !== "127.0.0.1") {
return location.protocol + "//" + location.host + "/";
}
if (info && info.ip) return "http://" + info.ip + ":" + info.port + "/";
return null;
}
function renderQr() {
var url = phoneUrl();
$("phone").hidden = !url || view === "closed" || view === "lost";
if (!url || url === shownUrl) return;
shownUrl = url;
var m = qrEncode(url), 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 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + (n + 8) + " " + (n + 8) +
'" shape-rendering="crispEdges"><path fill="#000" d="' + d + '"/></svg>';
setText("url", url.replace(/\/$/, ""));
}
function apply(data) {
info = data;
baseSince = data.since;
baseAt = Date.now();
view = data.active ? "on" : "off";
}
function request(method, path) {
return fetch(path, { method: method, cache: "no-store" })
.then(function (r) { if (!r.ok) throw r; return r.json(); });
}
function poll() {
clearTimeout(pollTimer);
request("GET", "/status")
.then(function (data) {
if (!busy) { apply(data); render(); }
pollTimer = setTimeout(poll, 5000);
})
.catch(function () {
if (view !== "closed") { view = "lost"; render(); }
});
}
function act(path) {
busy = true;
render();
return request("POST", path).then(
function (data) { busy = false; return data; },
function (e) { busy = false; throw e; });
}
toggle.addEventListener("click", function () {
if (view === "on" || view === "off") {
act(view === "on" ? "/off" : "/on")
.then(function (data) { apply(data); render(); })
.catch(poll);
} else {
view = "loading";
render();
poll();
}
});
quit.addEventListener("click", function () {
if (!quitTimer) {
quitTimer = setTimeout(function () { quitTimer = null; render(); }, 3000);
render();
return;
}
clearTimeout(quitTimer);
quitTimer = null;
clearTimeout(pollTimer);
act("/quit")
.then(function () { view = "closed"; render(); })
.catch(poll);
});
setInterval(tick, 1000);
poll();
})();
</script>
</body>
</html>
+113 -23
View File
@@ -1,11 +1,11 @@
/* KeepAwake (PS4) - keep the PS4 out of rest mode while this payload is running.
Same behaviour as the PS5 build: the idle timer is reset by periodically
calling sceSystemServicePowerTick(), and a local TCP port makes the
payload a toggle (sending it again stops the running instance).
calling sceSystemServicePowerTick(), and a control page is served on TCP
port 9031 (see web.h).
libPS4 has no select(), so the control socket is non-blocking and polled
once a second. */
ten times a second. */
#include "ps4.h"
@@ -16,10 +16,17 @@
#define KEEPAWAKE_PORT 9031
#define KEEPAWAKE_TICK_SECONDS 10
#define KEEPAWAKE_POLL_MS 100
#define SCE_NET_SO_RCVTIMEO 0x1006
#define SCE_NET_EAGAIN 35
#define SCE_NET_ERROR_EAGAIN 0x80410123
static int (*sceSystemServicePowerTick)(void);
static uint64_t g_started;
static int
resolve_power_tick(void) {
@@ -34,6 +41,60 @@ resolve_power_tick(void) {
}
static int
would_block(int rc) {
return (unsigned int)rc == SCE_NET_ERROR_EAGAIN || sce_net_errno == SCE_NET_EAGAIN;
}
#define WEB_CONSOLE "PS4"
#include "web.h"
static int
web_recv(int fd, void *buf, size_t len) {
int rc = sceNetRecv(fd, buf, len, 0);
if(rc < 0) {
return would_block(rc) ? WEB_AGAIN : WEB_ERROR;
}
return rc;
}
static int
web_send(int fd, const void *buf, size_t len) {
int rc = sceNetSend(fd, buf, len, 0);
if(rc < 0) {
return would_block(rc) ? WEB_AGAIN : WEB_ERROR;
}
return rc;
}
static void
web_sleep_ms(int ms) {
sceKernelUsleep(ms * 1000);
}
static long long
web_uptime(void) {
return (sceKernelGetProcessTime() - g_started) / 1000000;
}
static void
web_get_ip(char *buf, size_t size) {
SceNetCtlInfo info;
buf[0] = 0;
memset(&info, 0, sizeof info);
if(sceNetCtlGetInfo(SCE_NET_CTL_INFO_IP_ADDRESS, &info) == 0) {
snprintf(buf, size, "%s", info.ip_address);
}
}
static void
web_notify(const char *msg) {
printf_notification("%s", msg);
}
static void
make_addr(struct sockaddr_in *addr, unsigned int ip) {
memset(addr, 0, sizeof *addr);
@@ -44,22 +105,36 @@ make_addr(struct sockaddr_in *addr, unsigned int ip) {
}
/* Ask an already running instance to stop by connecting to its port. */
/* Ask an already running instance to quit. Returns 0 if it confirmed. */
static int
stop_running_instance(void) {
quit_running_instance(void) {
struct sockaddr_in addr;
int timeout_us = 3 * 1000 * 1000;
char resp[64];
int fd;
int rc;
int n;
if((fd = sceNetSocket("keepawake_stop", AF_INET, SOCK_STREAM, 0)) < 0) {
if((fd = sceNetSocket("keepawake_quit", AF_INET, SOCK_STREAM, 0)) < 0) {
return -1;
}
sceNetSetsockopt(fd, SOL_SOCKET, SCE_NET_SO_RCVTIMEO, &timeout_us, sizeof timeout_us);
make_addr(&addr, IP(127, 0, 0, 1));
rc = sceNetConnect(fd, (struct sockaddr*)&addr, sizeof addr);
if(sceNetConnect(fd, (struct sockaddr*)&addr, sizeof addr) < 0 ||
sceNetSend(fd, web_quit_request, sizeof web_quit_request - 1, 0) < 0) {
sceNetSocketClose(fd);
return -1;
}
n = sceNetRecv(fd, resp, sizeof resp - 1, 0);
sceNetSocketClose(fd);
return rc < 0 ? -1 : 0;
if(n <= 0) {
return -1;
}
resp[n] = 0;
return strstr(resp, " 200 ") ? 0 : -1;
}
@@ -78,7 +153,7 @@ open_control_socket(void) {
make_addr(&addr, IN_ADDR_ANY);
if(sceNetBind(fd, (struct sockaddr*)&addr, sizeof addr) < 0 ||
sceNetListen(fd, 1) < 0) {
sceNetListen(fd, 8) < 0) {
sceNetSocketClose(fd);
return -1;
}
@@ -89,8 +164,11 @@ open_control_socket(void) {
int
_main(struct thread *td) {
char ip[32];
int yes = 1;
int srv;
int cli;
int quit;
UNUSED(td);
@@ -98,6 +176,7 @@ _main(struct thread *td) {
initLibc();
initNetwork();
jailbreak();
sceNetCtlInit();
if(resolve_power_tick() != 0) {
printf_notification("Keep Awake failed to start:\n"
@@ -106,31 +185,42 @@ _main(struct thread *td) {
}
if((srv = open_control_socket()) < 0) {
// Port taken: an instance is already running, so toggle it off.
// The running instance shows the "disabled" toast itself.
if(stop_running_instance() == 0) {
// Port taken: an instance is already running, so close it.
// The running instance shows the "closed" toast itself.
if(quit_running_instance() == 0) {
return 0;
}
printf_notification("Keep Awake failed to start:\n"
"port %d unavailable", KEEPAWAKE_PORT);
"port %d is in use", KEEPAWAKE_PORT);
return -1;
}
printf_notification("Keep Awake " KEEPAWAKE_VERSION " enabled\n"
"Send the payload again to disable");
g_started = sceKernelGetProcessTime();
web_get_ip(ip, sizeof ip);
if(ip[0]) {
printf_notification("Keep Awake " KEEPAWAKE_VERSION " enabled\n"
"Control it at http://%s:%d", ip, KEEPAWAKE_PORT);
} else {
printf_notification("Keep Awake " KEEPAWAKE_VERSION " enabled\n"
"Control page on port %d", KEEPAWAKE_PORT);
}
while(1) {
sceSystemServicePowerTick();
ka_tick();
for(int i = 0; i < KEEPAWAKE_TICK_SECONDS; i++) {
// Any incoming connection is a request to stop.
if((cli = sceNetAccept(srv, NULL, NULL)) >= 0) {
sceNetSocketClose(cli);
printf_notification("Keep Awake " KEEPAWAKE_VERSION " disabled");
if((cli = sceNetAccept(srv, NULL, NULL)) >= 0) {
sceNetSetsockopt(cli, SOL_SOCKET, SO_NBIO, &yes, sizeof yes);
quit = web_handle_client(cli);
sceNetSocketClose(cli);
if(quit) {
printf_notification("Keep Awake " KEEPAWAKE_VERSION " closed");
sceNetSocketClose(srv);
return 0;
}
sceKernelSleep(1);
continue;
}
sceKernelUsleep(KEEPAWAKE_POLL_MS * 1000);
}
}
+129 -25
View File
@@ -1,14 +1,17 @@
/* KeepAwake - keep the PS5 out of rest mode while this payload is running.
The console's idle timer is reset by periodically calling
sceSystemServicePowerTick(). The payload also listens on a local TCP
port, which serves two purposes:
- it guarantees only one instance runs at a time, and
- sending the payload a second time (or connecting to the port)
stops the running instance, i.e. the payload works as a toggle. */
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,
- guarantees only one instance runs at a time, and
- lets a second copy of the payload close the running one. */
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdarg.h>
@@ -16,6 +19,7 @@
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
@@ -37,6 +41,18 @@ int sceKernelSendNotificationRequest(int, notify_request_t*, size_t, int);
int sceSystemServicePowerTick(void);
static time_t g_started;
static time_t
now_seconds(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec;
}
static void
notify(const char *fmt, ...) {
notify_request_t req;
@@ -48,30 +64,105 @@ notify(const char *fmt, ...) {
va_end(args);
printf("[KeepAwake] %s\n", req.message);
fflush(stdout);
sceKernelSendNotificationRequest(0, &req, sizeof req, 0);
}
/* Ask an already running instance to stop by connecting to its port. */
#define WEB_CONSOLE "PS5"
#include "web.h"
static int
stop_running_instance(void) {
web_recv(int fd, void *buf, size_t len) {
ssize_t rc = recv(fd, buf, len, 0);
if(rc < 0) {
return (errno == EAGAIN || errno == EWOULDBLOCK) ? WEB_AGAIN : WEB_ERROR;
}
return rc;
}
static int
web_send(int fd, const void *buf, size_t len) {
ssize_t rc = send(fd, buf, len, 0);
if(rc < 0) {
return (errno == EAGAIN || errno == EWOULDBLOCK) ? WEB_AGAIN : WEB_ERROR;
}
return rc;
}
static void
web_sleep_ms(int ms) {
usleep(ms * 1000);
}
static long long
web_uptime(void) {
return now_seconds() - g_started;
}
static void
web_get_ip(char *buf, size_t size) {
struct ifaddrs *ifs;
struct ifaddrs *it;
buf[0] = 0;
if(getifaddrs(&ifs) != 0) {
return;
}
for(it = ifs; it; it = it->ifa_next) {
if(!it->ifa_addr || it->ifa_addr->sa_family != AF_INET ||
!(it->ifa_flags & IFF_UP) || (it->ifa_flags & IFF_LOOPBACK)) {
continue;
}
inet_ntop(AF_INET, &((struct sockaddr_in*)it->ifa_addr)->sin_addr,
buf, size);
break;
}
freeifaddrs(ifs);
}
static void
web_notify(const char *msg) {
notify("%s", msg);
}
/* Ask an already running instance to quit. Returns 0 if it confirmed. */
static int
quit_running_instance(void) {
struct sockaddr_in addr;
struct timeval tv = {3, 0};
char resp[64];
ssize_t n;
int fd;
int rc;
if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
return -1;
}
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
bzero(&addr, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(KEEPAWAKE_PORT);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
rc = connect(fd, (struct sockaddr*)&addr, sizeof addr);
if(connect(fd, (struct sockaddr*)&addr, sizeof addr) != 0 ||
send(fd, web_quit_request, sizeof web_quit_request - 1, 0) < 0) {
close(fd);
return -1;
}
n = recv(fd, resp, sizeof resp - 1, 0);
close(fd);
return rc;
if(n <= 0) {
return -1;
}
resp[n] = 0;
return strstr(resp, " 200 ") ? 0 : -1;
}
@@ -93,7 +184,7 @@ open_control_socket(void) {
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(fd, (struct sockaddr*)&addr, sizeof addr) != 0 ||
listen(fd, 1) != 0) {
listen(fd, 8) != 0) {
int err = errno;
close(fd);
errno = err;
@@ -107,33 +198,43 @@ open_control_socket(void) {
int
main(void) {
struct timeval tv;
char ip[32];
fd_set fds;
int srv;
int cli;
int rc;
signal(SIGPIPE, SIG_IGN);
if((srv = open_control_socket()) < 0) {
if(errno == EADDRINUSE) {
// Port taken: an instance is already running, so toggle it off.
// The running instance shows the "disabled" toast itself.
if(stop_running_instance() == 0) {
// Port taken: an instance is already running, so close it.
// The running instance shows the "closed" toast itself.
if(quit_running_instance() == 0) {
return 0;
}
notify("Keep Awake failed to start:\nport %d is in use", KEEPAWAKE_PORT);
return -1;
}
notify("Keep Awake failed to start: %s", strerror(errno));
return -1;
}
notify("Keep Awake " KEEPAWAKE_VERSION " enabled\n"
"Send the payload again to disable");
g_started = now_seconds();
web_get_ip(ip, sizeof ip);
if(ip[0]) {
notify("Keep Awake " KEEPAWAKE_VERSION " enabled\n"
"Control it at http://%s:%d", ip, KEEPAWAKE_PORT);
} else {
notify("Keep Awake " KEEPAWAKE_VERSION " enabled\n"
"Control page on port %d", KEEPAWAKE_PORT);
}
while(1) {
sceSystemServicePowerTick();
FD_ZERO(&fds);
FD_SET(srv, &fds);
tv.tv_sec = KEEPAWAKE_TICK_SECONDS;
tv.tv_sec = ka_tick();
tv.tv_usec = 0;
rc = select(srv + 1, &fds, NULL, NULL, &tv);
@@ -146,13 +247,16 @@ main(void) {
}
if(rc > 0 && FD_ISSET(srv, &fds)) {
// Any incoming connection is a request to stop.
int cli = accept(srv, NULL, NULL);
if(cli >= 0) {
close(cli);
if((cli = accept(srv, NULL, NULL)) < 0) {
continue;
}
fcntl(cli, F_SETFL, fcntl(cli, F_GETFL) | O_NONBLOCK);
rc = web_handle_client(cli);
close(cli);
if(rc) {
notify("Keep Awake " KEEPAWAKE_VERSION " closed");
break;
}
notify("Keep Awake " KEEPAWAKE_VERSION " disabled");
break;
}
}
+244
View File
@@ -0,0 +1,244 @@
/* State and tiny HTTP server for the KeepAwake control page, shared by the
PS5 and PS4 builds.
Keep Awake can be on (resetting the idle timer) or off (paused, payload
still running so it can be turned back on from the page). Quitting ends
the payload.
Routes:
GET / the control page (src/index.html)
GET /status JSON status
POST /on start keeping the console awake
POST /off stop keeping it awake, but stay running
POST /quit end the payload
The platform source defines WEB_CONSOLE ("PS5" or "PS4") before including
this file, and implements the functions declared below. Client sockets are
expected to be non-blocking. */
#include "index_html.h"
#define WEB_AGAIN -1
#define WEB_ERROR -2
#define WEB_TIMEOUT_MS 2000
#define WEB_POLL_MS 10
/* Bytes read, 0 if the client closed, WEB_AGAIN if no data yet, or WEB_ERROR. */
static int web_recv(int fd, void *buf, size_t len);
/* Bytes sent, WEB_AGAIN if the socket is full, or WEB_ERROR. */
static int web_send(int fd, const void *buf, size_t len);
static void web_sleep_ms(int ms);
/* Seconds since the payload started. */
static long long web_uptime(void);
/* The console's LAN IP address as text, or an empty string if unknown. */
static void web_get_ip(char *buf, size_t size);
/* Show a toast on the console. */
static void web_notify(const char *msg);
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 void
ka_set_active(int active) {
if(active == ka_active) {
return;
}
ka_active = active;
ka_changed = web_uptime();
ka_next_tick = 0;
web_notify(active ? "Keep Awake turned on" : "Keep Awake turned off");
}
static int
web_starts_with(const char *s, size_t len, const char *prefix) {
size_t n = strlen(prefix);
return len >= n && memcmp(s, prefix, n) == 0;
}
static int
web_send_all(int fd, const void *buf, size_t len) {
const char *p = buf;
int waited = 0;
int rc;
while(len > 0) {
rc = web_send(fd, p, len);
if(rc == WEB_AGAIN) {
if((waited += WEB_POLL_MS) > WEB_TIMEOUT_MS) {
return -1;
}
web_sleep_ms(WEB_POLL_MS);
continue;
}
if(rc <= 0) {
return -1;
}
p += rc;
len -= rc;
}
return 0;
}
static void
web_respond(int fd, const char *status, const char *type,
const void *body, size_t len) {
char head[256];
int n;
n = snprintf(head, sizeof head,
"HTTP/1.1 %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %d\r\n"
"Cache-Control: no-store\r\n"
"Connection: close\r\n"
"\r\n", status, type, (int)len);
if(web_send_all(fd, head, n) == 0 && len > 0) {
web_send_all(fd, body, len);
}
}
static void
web_respond_json(int fd, const char *status, const char *json) {
web_respond(fd, status, "application/json", json, strlen(json));
}
static void
web_respond_status(int fd) {
char json[320];
char ip[32];
web_get_ip(ip, sizeof ip);
snprintf(json, sizeof json,
"{\"version\":\"%s\",\"console\":\"%s\",\"active\":%s,"
"\"uptime\":%lld,\"since\":%lld,\"interval\":%d,"
"\"ip\":\"%s\",\"port\":%d}",
KEEPAWAKE_VERSION, WEB_CONSOLE, ka_active ? "true" : "false",
web_uptime(), web_uptime() - ka_changed, KEEPAWAKE_TICK_SECONDS,
ip, KEEPAWAKE_PORT);
web_respond_json(fd, "200 OK", json);
}
/* Read the request headers, giving up after WEB_TIMEOUT_MS so a slow or idle
client can't hold up the keep-awake loop. Returns the bytes read. */
static size_t
web_read_request(int fd, char *buf, size_t size) {
size_t len = 0;
int waited = 0;
int rc;
while(len < size - 1) {
rc = web_recv(fd, buf + len, size - 1 - len);
if(rc == WEB_AGAIN) {
if((waited += WEB_POLL_MS) > WEB_TIMEOUT_MS) {
break;
}
web_sleep_ms(WEB_POLL_MS);
continue;
}
if(rc <= 0) {
break;
}
len += rc;
buf[len] = 0;
if(strstr(buf, "\r\n\r\n")) {
break;
}
}
buf[len] = 0;
return len;
}
/* Handle one client connection. Returns 1 if the payload should quit. */
static int
web_handle_client(int fd) {
char req[1024];
size_t len;
if(!(len = web_read_request(fd, req, sizeof req))) {
return 0;
}
if(web_starts_with(req, len, "GET / ") ||
web_starts_with(req, len, "GET /index.html ")) {
web_respond(fd, "200 OK", "text/html; charset=utf-8",
index_html, index_html_len);
} else if(web_starts_with(req, len, "GET /status ")) {
web_respond_status(fd);
} else if(web_starts_with(req, len, "POST /on ")) {
ka_set_active(1);
web_respond_status(fd);
} else if(web_starts_with(req, len, "POST /off ")) {
ka_set_active(0);
web_respond_status(fd);
} else if(web_starts_with(req, len, "POST /quit ")) {
web_respond_json(fd, "200 OK", "{\"quit\":true}");
return 1;
} else if(web_starts_with(req, len, "GET /on ") ||
web_starts_with(req, len, "GET /off ") ||
web_starts_with(req, len, "GET /quit ")) {
// Only POST changes anything, so link previews and prefetching can't.
web_respond_json(fd, "405 Method Not Allowed",
"{\"error\":\"use POST\"}");
} else if(web_starts_with(req, len, "GET /favicon.ico ")) {
web_respond(fd, "204 No Content", "text/plain", "", 0);
} else {
web_respond_json(fd, "404 Not Found", "{\"error\":\"not found\"}");
}
return 0;
}
/* Call regularly from the main loop: resets the idle timer when it's due.
Returns the seconds until it next needs calling. */
static int
ka_tick(void) {
long long now = web_uptime();
if(!ka_active) {
return KEEPAWAKE_TICK_SECONDS;
}
if(now >= ka_next_tick) {
sceSystemServicePowerTick();
ka_next_tick = now + KEEPAWAKE_TICK_SECONDS;
}
return (int)(ka_next_tick - now);
}
/* The request a new instance sends to make the running one quit. */
static const char web_quit_request[] =
"POST /quit HTTP/1.0\r\n"
"Host: localhost\r\n"
"Content-Length: 0\r\n"
"\r\n";
+19
View File
@@ -0,0 +1,19 @@
/* Stand-ins for the PS5 system calls, so src/ps5.c can run on a computer
(see `make host`). */
#include <stdio.h>
#include <stddef.h>
int
sceKernelSendNotificationRequest(int device, void *req, size_t size, int blocking) {
return 0;
}
int
sceSystemServicePowerTick(void) {
printf("[KeepAwake] power tick\n");
fflush(stdout);
return 0;
}