@myerscarpenter/quest-dev 1.4.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +7 -0
- package/.github/workflows/docs.yml +45 -0
- package/.github/workflows/publish.yml +11 -1
- package/README.md +27 -0
- package/build/cast/decoder.d.ts +48 -0
- package/build/cast/decoder.d.ts.map +1 -0
- package/build/cast/decoder.js +152 -0
- package/build/cast/decoder.js.map +1 -0
- package/build/cast/session.d.ts +87 -0
- package/build/cast/session.d.ts.map +1 -0
- package/build/cast/session.js +565 -0
- package/build/cast/session.js.map +1 -0
- package/build/commands/logcat.d.ts.map +1 -1
- package/build/commands/logcat.js +7 -6
- package/build/commands/logcat.js.map +1 -1
- package/build/commands/screenshot.d.ts.map +1 -1
- package/build/commands/screenshot.js +17 -20
- package/build/commands/screenshot.js.map +1 -1
- package/build/commands/stay-awake.d.ts +2 -15
- package/build/commands/stay-awake.d.ts.map +1 -1
- package/build/commands/stay-awake.js +14 -77
- package/build/commands/stay-awake.js.map +1 -1
- package/build/daemon/cast-manager.d.ts +42 -0
- package/build/daemon/cast-manager.d.ts.map +1 -0
- package/build/daemon/cast-manager.js +243 -0
- package/build/daemon/cast-manager.js.map +1 -0
- package/build/daemon/client.d.ts +40 -0
- package/build/daemon/client.d.ts.map +1 -0
- package/build/daemon/client.js +133 -0
- package/build/daemon/client.js.map +1 -0
- package/build/daemon/daemon.d.ts +20 -0
- package/build/daemon/daemon.d.ts.map +1 -0
- package/build/daemon/daemon.js +130 -0
- package/build/daemon/daemon.js.map +1 -0
- package/build/daemon/deploy.d.ts +44 -0
- package/build/daemon/deploy.d.ts.map +1 -0
- package/build/daemon/deploy.js +230 -0
- package/build/daemon/deploy.js.map +1 -0
- package/build/daemon/logcat-manager.d.ts +39 -0
- package/build/daemon/logcat-manager.d.ts.map +1 -0
- package/build/daemon/logcat-manager.js +194 -0
- package/build/daemon/logcat-manager.js.map +1 -0
- package/build/daemon/server.d.ts +19 -0
- package/build/daemon/server.d.ts.map +1 -0
- package/build/daemon/server.js +482 -0
- package/build/daemon/server.js.map +1 -0
- package/build/daemon/stay-awake-manager.d.ts +22 -0
- package/build/daemon/stay-awake-manager.d.ts.map +1 -0
- package/build/daemon/stay-awake-manager.js +74 -0
- package/build/daemon/stay-awake-manager.js.map +1 -0
- package/build/index.js +272 -45
- package/build/index.js.map +1 -1
- package/build/public/dashboard.js +749 -0
- package/build/public/index.html +12 -0
- package/build/public/style.css +106 -0
- package/build/utils/adb.d.ts +6 -0
- package/build/utils/adb.d.ts.map +1 -1
- package/build/utils/adb.js +62 -66
- package/build/utils/adb.js.map +1 -1
- package/build/utils/casting-apk.d.ts +40 -0
- package/build/utils/casting-apk.d.ts.map +1 -0
- package/build/utils/casting-apk.js +252 -0
- package/build/utils/casting-apk.js.map +1 -0
- package/build/utils/config.d.ts +5 -3
- package/build/utils/config.d.ts.map +1 -1
- package/build/utils/config.js +18 -38
- package/build/utils/config.js.map +1 -1
- package/build/utils/exec.d.ts +5 -0
- package/build/utils/exec.d.ts.map +1 -1
- package/build/utils/exec.js +17 -0
- package/build/utils/exec.js.map +1 -1
- package/build/utils/filename.d.ts +7 -1
- package/build/utils/filename.d.ts.map +1 -1
- package/build/utils/filename.js +17 -2
- package/build/utils/filename.js.map +1 -1
- package/build/utils/filename.test.js +33 -1
- package/build/utils/filename.test.js.map +1 -1
- package/build/utils/jpeg-comment.d.ts +14 -0
- package/build/utils/jpeg-comment.d.ts.map +1 -0
- package/build/utils/jpeg-comment.js +28 -0
- package/build/utils/jpeg-comment.js.map +1 -0
- package/build/utils/test-properties.d.ts +34 -0
- package/build/utils/test-properties.d.ts.map +1 -0
- package/build/utils/test-properties.js +73 -0
- package/build/utils/test-properties.js.map +1 -0
- package/package.json +11 -5
- package/packages/cast2-protocol/README.md +86 -0
- package/packages/cast2-protocol/docs/_config.yml +4 -0
- package/packages/cast2-protocol/docs/feature-flags.md +102 -0
- package/packages/cast2-protocol/docs/index.md +24 -0
- package/packages/cast2-protocol/docs/open-investigations.md +149 -0
- package/packages/cast2-protocol/docs/protocol.md +602 -0
- package/packages/cast2-protocol/package.json +46 -0
- package/packages/cast2-protocol/src/constants.ts +65 -0
- package/packages/cast2-protocol/src/index.ts +7 -0
- package/packages/cast2-protocol/src/mgik.ts +69 -0
- package/packages/cast2-protocol/src/mud.ts +294 -0
- package/packages/cast2-protocol/src/pose.ts +99 -0
- package/packages/cast2-protocol/src/resolutions.ts +34 -0
- package/packages/cast2-protocol/src/types.ts +64 -0
- package/packages/cast2-protocol/src/xrsp.ts +73 -0
- package/packages/cast2-protocol/tests/mgik.test.ts +80 -0
- package/packages/cast2-protocol/tests/mud.test.ts +295 -0
- package/packages/cast2-protocol/tests/pose.test.ts +173 -0
- package/packages/cast2-protocol/tests/xrsp.test.ts +90 -0
- package/packages/cast2-protocol/tsconfig.json +20 -0
- package/pnpm-workspace.yaml +2 -0
- package/src/cast/decoder.ts +178 -0
- package/src/cast/session.ts +708 -0
- package/src/commands/logcat.ts +6 -5
- package/src/commands/screenshot.ts +19 -13
- package/src/commands/stay-awake.ts +22 -91
- package/src/daemon/adbkit-apkreader.d.ts +14 -0
- package/src/daemon/cast-manager.ts +282 -0
- package/src/daemon/client.ts +166 -0
- package/src/daemon/daemon.ts +169 -0
- package/src/daemon/deploy.ts +307 -0
- package/src/daemon/logcat-manager.ts +229 -0
- package/src/daemon/server.ts +595 -0
- package/src/daemon/stay-awake-manager.ts +83 -0
- package/src/index.ts +326 -56
- package/src/public/dashboard.js +288 -0
- package/src/public/index.html +12 -0
- package/src/public/style.css +106 -0
- package/src/utils/adb.ts +70 -57
- package/src/utils/casting-apk.ts +276 -0
- package/src/utils/config.ts +18 -36
- package/src/utils/exec.ts +20 -0
- package/src/utils/filename.test.ts +41 -1
- package/src/utils/filename.ts +18 -2
- package/src/utils/jpeg-comment.ts +30 -0
- package/src/utils/test-properties.ts +94 -0
- package/tests/cast/auto-layer.test.ts +87 -0
- package/tests/cast/decoder.test.ts +82 -0
- package/tests/cast/session-restart.test.ts +107 -0
- package/tests/config.test.ts +17 -22
- package/tests/daemon/api-status.test.ts +82 -0
- package/tests/daemon/cast-manager.test.ts +69 -0
- package/tests/daemon/mjpeg-stream.test.ts +144 -0
- package/tests/daemon/pose-endpoint.test.ts +63 -0
- package/tests/daemon/start-guard.test.ts +77 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { h, render } from "preact";
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from "preact/hooks";
|
|
3
|
+
import htm from "htm";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
// --- API helper ---
|
|
8
|
+
async function api(method, path, body) {
|
|
9
|
+
const opts = { method };
|
|
10
|
+
if (body) {
|
|
11
|
+
opts.headers = { "Content-Type": "application/json" };
|
|
12
|
+
opts.body = JSON.stringify(body);
|
|
13
|
+
}
|
|
14
|
+
const r = await fetch(path, opts);
|
|
15
|
+
if (!r.ok) {
|
|
16
|
+
let msg;
|
|
17
|
+
try { const j = await r.json(); msg = j.error ?? r.statusText; } catch { msg = r.statusText; }
|
|
18
|
+
throw new Error(`HTTP ${r.status}: ${msg}`);
|
|
19
|
+
}
|
|
20
|
+
return r.json();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Helper: pick button class based on whether it matches current server state
|
|
24
|
+
function bc(active) { return active ? "" : "secondary"; }
|
|
25
|
+
|
|
26
|
+
// --- App ---
|
|
27
|
+
function App() {
|
|
28
|
+
// Single source of truth: server status (polled every second)
|
|
29
|
+
const [s, setS] = useState({ connected: false, running: false });
|
|
30
|
+
const [toast, setToast] = useState(null);
|
|
31
|
+
const toastTimer = useRef(null);
|
|
32
|
+
|
|
33
|
+
const active = s.connected && s.running;
|
|
34
|
+
const pose = s.pose || {};
|
|
35
|
+
|
|
36
|
+
const showToast = useCallback((msg) => {
|
|
37
|
+
setToast(msg);
|
|
38
|
+
clearTimeout(toastTimer.current);
|
|
39
|
+
toastTimer.current = setTimeout(() => setToast(null), 2000);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
// SSE — single source of truth for all state
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
let es;
|
|
45
|
+
function connect() {
|
|
46
|
+
es = new EventSource("/cast/events");
|
|
47
|
+
es.addEventListener("status", (e) => {
|
|
48
|
+
setS(JSON.parse(e.data));
|
|
49
|
+
});
|
|
50
|
+
es.addEventListener("toast", (e) => {
|
|
51
|
+
showToast(JSON.parse(e.data).message);
|
|
52
|
+
});
|
|
53
|
+
es.onerror = () => {
|
|
54
|
+
setS((prev) => ({ ...prev, connected: false, running: false }));
|
|
55
|
+
es.close();
|
|
56
|
+
setTimeout(connect, 2000);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
connect();
|
|
60
|
+
return () => es?.close();
|
|
61
|
+
}, [showToast]);
|
|
62
|
+
|
|
63
|
+
// Double-buffered frame refresh: fetch next frame while current displays
|
|
64
|
+
const imgRef = useRef(null);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!active) return;
|
|
67
|
+
let run = true;
|
|
68
|
+
// Two offscreen buffers, alternating
|
|
69
|
+
const buf = [new Image(), new Image()];
|
|
70
|
+
let idx = 0;
|
|
71
|
+
function fetch_next() {
|
|
72
|
+
if (!run) return;
|
|
73
|
+
const img = buf[idx];
|
|
74
|
+
idx = 1 - idx;
|
|
75
|
+
img.onload = () => {
|
|
76
|
+
if (!run) return;
|
|
77
|
+
if (imgRef.current) imgRef.current.src = img.src;
|
|
78
|
+
requestAnimationFrame(fetch_next);
|
|
79
|
+
};
|
|
80
|
+
img.onerror = () => { if (run) setTimeout(fetch_next, 500); };
|
|
81
|
+
img.src = "/cast/screenshot?t=" + Date.now();
|
|
82
|
+
}
|
|
83
|
+
fetch_next();
|
|
84
|
+
return () => { run = false; };
|
|
85
|
+
}, [active]);
|
|
86
|
+
|
|
87
|
+
// Keyboard + mouse (imperative)
|
|
88
|
+
const overlayRef = useRef(null);
|
|
89
|
+
const crosshairRef = useRef(null);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const keysDown = new Set();
|
|
93
|
+
let keyLoop = false, dragging = false, dx0, dy0;
|
|
94
|
+
|
|
95
|
+
function spd() { return parseFloat(document.getElementById("move-speed")?.value) || 0.3; }
|
|
96
|
+
function look() { return parseFloat(document.getElementById("look-speed")?.value) || 0.15; }
|
|
97
|
+
|
|
98
|
+
function tick() {
|
|
99
|
+
if (keysDown.size === 0) { keyLoop = false; return; }
|
|
100
|
+
const m = spd(), l = look();
|
|
101
|
+
let fwd = 0, str = 0, yaw = 0, pit = 0, up = 0;
|
|
102
|
+
if (keysDown.has("w") || keysDown.has("arrowup")) fwd += m;
|
|
103
|
+
if (keysDown.has("s") || keysDown.has("arrowdown")) fwd -= m;
|
|
104
|
+
if (keysDown.has("a") || keysDown.has("arrowleft")) str -= m;
|
|
105
|
+
if (keysDown.has("d") || keysDown.has("arrowright")) str += m;
|
|
106
|
+
if (keysDown.has("q")) up -= m;
|
|
107
|
+
if (keysDown.has("e")) up += m;
|
|
108
|
+
if (keysDown.has("j")) yaw -= l;
|
|
109
|
+
if (keysDown.has("l")) yaw += l;
|
|
110
|
+
if (keysDown.has("i")) pit += l;
|
|
111
|
+
if (keysDown.has("k")) pit -= l;
|
|
112
|
+
if (fwd || str || yaw || pit || up)
|
|
113
|
+
api("POST", "/cast/pose", { dz: fwd, dx: str, dy: up, d_yaw: yaw, d_pitch: pit }).catch((e) => showToast(e.message));
|
|
114
|
+
setTimeout(tick, 80);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const kd = (e) => {
|
|
118
|
+
if (e.target.tagName === "INPUT") return;
|
|
119
|
+
if (e.repeat) return;
|
|
120
|
+
keysDown.add(e.key.toLowerCase());
|
|
121
|
+
if (["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," "].includes(e.key)) e.preventDefault();
|
|
122
|
+
if (e.key === "p" || e.key === "P") { doScreenshot(); return; }
|
|
123
|
+
if (e.key === " " || e.key === "Enter") { doClick(); return; }
|
|
124
|
+
if (e.key === "Escape") { doResetPose(); return; }
|
|
125
|
+
if (!keyLoop) { keyLoop = true; tick(); }
|
|
126
|
+
};
|
|
127
|
+
const ku = (e) => { if (e.target.tagName === "INPUT") return; keysDown.delete(e.key.toLowerCase()); };
|
|
128
|
+
const md = (e) => { if (e.button !== 0) return; dragging = true; dx0 = e.clientX; dy0 = e.clientY; if (crosshairRef.current) crosshairRef.current.style.display = "block"; };
|
|
129
|
+
const mm = (e) => {
|
|
130
|
+
if (!dragging) return;
|
|
131
|
+
const dx = e.clientX - dx0, dy = e.clientY - dy0;
|
|
132
|
+
dx0 = e.clientX; dy0 = e.clientY;
|
|
133
|
+
const sn = look();
|
|
134
|
+
const yaw = dx * sn * 0.02, pitch = dy * sn * 0.02;
|
|
135
|
+
if (yaw || pitch) api("POST", "/cast/pose", { d_yaw: yaw, d_pitch: pitch }).catch((e) => showToast(e.message));
|
|
136
|
+
};
|
|
137
|
+
const mu = () => { dragging = false; if (crosshairRef.current) crosshairRef.current.style.display = "none"; };
|
|
138
|
+
const ctx = (e) => { e.preventDefault(); doClick(); };
|
|
139
|
+
|
|
140
|
+
document.addEventListener("keydown", kd);
|
|
141
|
+
document.addEventListener("keyup", ku);
|
|
142
|
+
window.addEventListener("mousemove", mm);
|
|
143
|
+
window.addEventListener("mouseup", mu);
|
|
144
|
+
const ov = overlayRef.current;
|
|
145
|
+
if (ov) { ov.addEventListener("mousedown", md); ov.addEventListener("contextmenu", ctx); }
|
|
146
|
+
return () => {
|
|
147
|
+
document.removeEventListener("keydown", kd);
|
|
148
|
+
document.removeEventListener("keyup", ku);
|
|
149
|
+
window.removeEventListener("mousemove", mm);
|
|
150
|
+
window.removeEventListener("mouseup", mu);
|
|
151
|
+
if (ov) { ov.removeEventListener("mousedown", md); ov.removeEventListener("contextmenu", ctx); }
|
|
152
|
+
};
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
// Actions
|
|
156
|
+
const doClick = async () => {
|
|
157
|
+
try { const r = await api("POST", "/cast/click"); if (r?.ok) showToast("Click sent"); }
|
|
158
|
+
catch (e) { showToast("Click failed: " + e.message); }
|
|
159
|
+
};
|
|
160
|
+
const doScreenshot = () => {
|
|
161
|
+
const a = document.createElement("a");
|
|
162
|
+
a.href = "/cast/screenshot";
|
|
163
|
+
a.download = "quest-" + new Date().toISOString().replace(/[:.]/g, "-") + ".jpg";
|
|
164
|
+
a.click();
|
|
165
|
+
showToast("Screenshot saved");
|
|
166
|
+
};
|
|
167
|
+
const doResetPose = async () => {
|
|
168
|
+
try { await api("POST", "/cast/reset-view"); showToast("Back to HMD view"); }
|
|
169
|
+
catch (e) { showToast("Reset failed: " + e.message); }
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Derive display state from server
|
|
173
|
+
const curRes = s.width && s.height ? s.width + "x" + s.height : "";
|
|
174
|
+
const curEye = s.eye || "left";
|
|
175
|
+
|
|
176
|
+
return html`
|
|
177
|
+
<header>
|
|
178
|
+
<div id="status-dot" class=${active ? "ok" : ""}></div>
|
|
179
|
+
<h1>Quest Cast</h1>
|
|
180
|
+
${active ? html`
|
|
181
|
+
<div id="status-bar">
|
|
182
|
+
<span><span class="val">${s.fps ?? "--"}</span> fps</span>
|
|
183
|
+
<span><span class="val">${s.width ? s.width + "\u00d7" + s.height : "--"}</span></span>
|
|
184
|
+
<span><span class="val">${s.frame_count?.toLocaleString() ?? "--"}</span> frames</span>
|
|
185
|
+
<span><span class="val">${s.bytes ? (s.bytes / 1048576).toFixed(1) : "--"}</span> MB</span>
|
|
186
|
+
<button onclick=${() => { if (confirm("Stop casting?")) api("POST", "/cast/stop"); }}>Stop</button>
|
|
187
|
+
</div>
|
|
188
|
+
` : html`
|
|
189
|
+
<div id="header-cast-btn">
|
|
190
|
+
<button onclick=${() => api("POST", "/cast/start")}>Cast</button>
|
|
191
|
+
</div>
|
|
192
|
+
`}
|
|
193
|
+
</header>
|
|
194
|
+
|
|
195
|
+
<div class="main">
|
|
196
|
+
<div class="video-pane">
|
|
197
|
+
<${VideoPane} active=${active} imgRef=${imgRef} crosshairRef=${crosshairRef} overlayRef=${overlayRef} />
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div class="sidebar">
|
|
201
|
+
<${Panel} title="Movement">
|
|
202
|
+
<div class="btn-row" style="justify-content:center;">
|
|
203
|
+
<div style="display:grid; grid-template-columns: repeat(3,40px); grid-template-rows: repeat(2,34px); gap:4px;">
|
|
204
|
+
<div></div>
|
|
205
|
+
<button class="secondary" onclick=${() => api("POST", "/cast/pose", { dz: parseFloat(document.getElementById("move-speed")?.value) || 0.3 })}>\u25B2</button>
|
|
206
|
+
<div></div>
|
|
207
|
+
<button class="secondary" onclick=${() => api("POST", "/cast/pose", { dx: -(parseFloat(document.getElementById("move-speed")?.value) || 0.3) })}>\u25C0</button>
|
|
208
|
+
<button class="secondary" onclick=${() => api("POST", "/cast/pose", { dz: -(parseFloat(document.getElementById("move-speed")?.value) || 0.3) })}>\u25BC</button>
|
|
209
|
+
<button class="secondary" onclick=${() => api("POST", "/cast/pose", { dx: parseFloat(document.getElementById("move-speed")?.value) || 0.3 })}>\u25B6</button>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="speed-row">
|
|
213
|
+
<label>Speed</label>
|
|
214
|
+
<input type="number" id="move-speed" value="0.3" min="0.01" max="2" step="0.05" />
|
|
215
|
+
<label>Look</label>
|
|
216
|
+
<input type="number" id="look-speed" value="0.15" min="0.01" max="1" step="0.01" />
|
|
217
|
+
</div>
|
|
218
|
+
<//>
|
|
219
|
+
|
|
220
|
+
<${Panel} title="Offset from HMD">
|
|
221
|
+
<div class="pose-readout">
|
|
222
|
+
<span>${pose.x?.toFixed(2) ?? "0"}, ${pose.y?.toFixed(2) ?? "0"}, ${pose.z?.toFixed(2) ?? "0"}</span>
|
|
223
|
+
<span>${pose.yaw_deg?.toFixed(1) ?? "0"}\u00b0 yaw, ${pose.pitch_deg?.toFixed(1) ?? "0"}\u00b0 pitch</span>
|
|
224
|
+
<span>loop: ${s.pose_loop ? "~27 Hz" : "off"}</span>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="btn-row">
|
|
227
|
+
<button class="secondary" style="flex:1;" onclick=${async () => { const r = await api("POST", "/cast/pose", { x: 0, y: 0, z: 0, yaw: 0, pitch: 0 }); if (r?.ok) showToast("Origin"); }}>Origin</button>
|
|
228
|
+
<button class=${s.pose_loop ? "" : "secondary"} style="flex:1;" onclick=${doResetPose}>${s.pose_loop ? "Back to HMD" : "Reset"}</button>
|
|
229
|
+
</div>
|
|
230
|
+
<//>
|
|
231
|
+
|
|
232
|
+
<${Panel} title="Display">
|
|
233
|
+
<div class="btn-row">
|
|
234
|
+
<button class=${bc(curRes === "2064x1162")} onclick=${() => api("POST", "/cast/config", { resolution: "native" })}>Native</button>
|
|
235
|
+
<button class=${bc(curRes === "1920x1080")} onclick=${() => api("POST", "/cast/config", { resolution: "1080p" })}>1080p</button>
|
|
236
|
+
<button class=${bc(curRes === "1280x720")} onclick=${() => api("POST", "/cast/config", { resolution: "720p" })}>720p</button>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="btn-row" style="margin-top:6px;">
|
|
239
|
+
<button class=${bc(curEye === "left")} onclick=${() => api("POST", "/cast/eye", { mode: "left" })}>L Eye</button>
|
|
240
|
+
<button class=${bc(curEye === "right")} onclick=${() => api("POST", "/cast/eye", { mode: "right" })}>R Eye</button>
|
|
241
|
+
<button class=${bc(curEye === "stereo")} onclick=${() => api("POST", "/cast/eye", { mode: "stereo" })}>Stereo</button>
|
|
242
|
+
</div>
|
|
243
|
+
<//>
|
|
244
|
+
|
|
245
|
+
<${Panel} title="Actions">
|
|
246
|
+
<div class="btn-row">
|
|
247
|
+
<button style="flex:1;" onclick=${doClick}>Click</button>
|
|
248
|
+
<button class="secondary" style="flex:1;" onclick=${() => api("POST", "/cast/home")}>Home</button>
|
|
249
|
+
</div>
|
|
250
|
+
<div class="btn-row" style="margin-top:6px;">
|
|
251
|
+
<button style="flex:1;" onclick=${doScreenshot}>Screenshot</button>
|
|
252
|
+
<button class="secondary" style="flex:1;" onclick=${() => window.open("/cast/stream", "_blank")}>MJPEG</button>
|
|
253
|
+
</div>
|
|
254
|
+
<//>
|
|
255
|
+
|
|
256
|
+
<${Panel} title="Keys">
|
|
257
|
+
<div class="keybind-hint">
|
|
258
|
+
<b>WASD</b> / <b>Arrows</b> move \u00a0 <b>IJKL</b> look \u00a0 <b>QE</b> up/down<br />
|
|
259
|
+
<b>Drag</b> free look \u00a0 <b>Right-click</b> click<br />
|
|
260
|
+
<b>Space</b> click \u00a0 <b>P</b> screenshot \u00a0 <b>Esc</b> back to HMD<br />
|
|
261
|
+
Movement overrides HMD tracking from origin
|
|
262
|
+
</div>
|
|
263
|
+
<//>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
${toast && html`<div class="toast show">${toast}</div>`}
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function VideoPane({ active, imgRef, crosshairRef, overlayRef }) {
|
|
272
|
+
if (!active) {
|
|
273
|
+
return html`<div class="video-placeholder">
|
|
274
|
+
<button onclick=${() => api("POST", "/cast/start")}>Start Casting</button>
|
|
275
|
+
</div>`;
|
|
276
|
+
}
|
|
277
|
+
return html`
|
|
278
|
+
<img id="live-frame" ref=${imgRef} alt="Quest view" />
|
|
279
|
+
<div id="crosshair" ref=${crosshairRef}></div>
|
|
280
|
+
<div id="drag-overlay" ref=${overlayRef}></div>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function Panel({ title, children }) {
|
|
285
|
+
return html`<div class="panel"><h2>${title}</h2>${children}</div>`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
render(html`<${App} />`, document.body);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Quest Cast</title>
|
|
7
|
+
<link rel="stylesheet" href="/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<script src="/dashboard.js"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0e0e10; --surface: #18181b; --border: #2d2d33;
|
|
3
|
+
--text: #efeff1; --dim: #898991; --accent: #6d5bff;
|
|
4
|
+
--accent2: #9147ff; --green: #00c853; --red: #f44;
|
|
5
|
+
}
|
|
6
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
7
|
+
body {
|
|
8
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
9
|
+
background: var(--bg); color: var(--text);
|
|
10
|
+
display: flex; flex-direction: column; height: 100vh; overflow: hidden;
|
|
11
|
+
user-select: none;
|
|
12
|
+
}
|
|
13
|
+
header {
|
|
14
|
+
display: flex; align-items: center; gap: 12px;
|
|
15
|
+
padding: 8px 16px; background: var(--surface); border-bottom: 1px solid var(--border);
|
|
16
|
+
flex-shrink: 0;
|
|
17
|
+
}
|
|
18
|
+
header h1 { font-size: 16px; font-weight: 600; }
|
|
19
|
+
#header-cast-btn { margin-left: auto; }
|
|
20
|
+
#status-bar {
|
|
21
|
+
display: flex; align-items: center; gap: 16px; margin-left: auto; font-size: 12px; color: var(--dim);
|
|
22
|
+
}
|
|
23
|
+
#status-bar .val { color: var(--text); font-variant-numeric: tabular-nums; }
|
|
24
|
+
#header-stop-btn { margin-left: 8px; padding: 4px 14px; font-size: 12px; }
|
|
25
|
+
#status-dot {
|
|
26
|
+
width: 8px; height: 8px; border-radius: 50%; background: var(--red);
|
|
27
|
+
align-self: center;
|
|
28
|
+
}
|
|
29
|
+
#status-dot.ok { background: var(--green); }
|
|
30
|
+
.main { display: flex; flex: 1; overflow: hidden; }
|
|
31
|
+
.video-pane {
|
|
32
|
+
flex: 1; display: flex; align-items: center; justify-content: center;
|
|
33
|
+
background: #000; position: relative; min-width: 0;
|
|
34
|
+
}
|
|
35
|
+
#live-frame {
|
|
36
|
+
max-width: 100%; max-height: 100%; object-fit: contain;
|
|
37
|
+
image-rendering: auto;
|
|
38
|
+
}
|
|
39
|
+
#crosshair {
|
|
40
|
+
position: absolute; pointer-events: none;
|
|
41
|
+
width: 24px; height: 24px;
|
|
42
|
+
border: 2px solid rgba(255,255,255,0.4); border-radius: 50%;
|
|
43
|
+
top: 50%; left: 50%; transform: translate(-50%,-50%);
|
|
44
|
+
display: none;
|
|
45
|
+
}
|
|
46
|
+
#crosshair::before, #crosshair::after {
|
|
47
|
+
content: ''; position: absolute; background: rgba(255,255,255,0.4);
|
|
48
|
+
}
|
|
49
|
+
#crosshair::before { width: 1px; height: 8px; top: 50%; left: 50%; transform: translate(-50%,-50%); }
|
|
50
|
+
#crosshair::after { width: 8px; height: 1px; top: 50%; left: 50%; transform: translate(-50%,-50%); }
|
|
51
|
+
.sidebar {
|
|
52
|
+
width: 260px; flex-shrink: 0; background: var(--surface);
|
|
53
|
+
border-left: 1px solid var(--border); display: flex; flex-direction: column;
|
|
54
|
+
overflow-y: auto;
|
|
55
|
+
}
|
|
56
|
+
.panel { padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
|
57
|
+
.panel h2 {
|
|
58
|
+
font-size: 10px; text-transform: uppercase; letter-spacing: .08em;
|
|
59
|
+
color: var(--dim); margin-bottom: 6px;
|
|
60
|
+
}
|
|
61
|
+
button {
|
|
62
|
+
background: var(--accent); color: #fff; border: none; border-radius: 6px;
|
|
63
|
+
padding: 5px 10px; font-size: 11px; cursor: pointer; font-weight: 500;
|
|
64
|
+
transition: opacity .15s;
|
|
65
|
+
}
|
|
66
|
+
button:hover { opacity: .85; }
|
|
67
|
+
button:active { opacity: .7; }
|
|
68
|
+
button.secondary { background: var(--border); }
|
|
69
|
+
button.danger { background: var(--red); }
|
|
70
|
+
.btn-row { display: flex; gap: 4px; }
|
|
71
|
+
.speed-row {
|
|
72
|
+
display: flex; align-items: center; gap: 6px; margin-top: 6px;
|
|
73
|
+
font-size: 10px; color: var(--dim);
|
|
74
|
+
}
|
|
75
|
+
.speed-row input {
|
|
76
|
+
width: 44px; background: var(--bg); color: var(--text);
|
|
77
|
+
border: 1px solid var(--border); border-radius: 4px; padding: 2px 4px;
|
|
78
|
+
font-size: 10px; text-align: center;
|
|
79
|
+
}
|
|
80
|
+
.pose-readout {
|
|
81
|
+
font-size: 10px; font-family: monospace; line-height: 1.5; color: var(--dim);
|
|
82
|
+
margin-bottom: 6px;
|
|
83
|
+
}
|
|
84
|
+
.pose-readout span { display: block; }
|
|
85
|
+
.pose-readout .val { color: var(--text); }
|
|
86
|
+
.keybind-hint {
|
|
87
|
+
font-size: 10px; color: var(--dim); line-height: 1.6;
|
|
88
|
+
}
|
|
89
|
+
.toast {
|
|
90
|
+
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
|
|
91
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
|
92
|
+
padding: 8px 16px; font-size: 13px; opacity: 0; transition: opacity .3s;
|
|
93
|
+
pointer-events: none; z-index: 100;
|
|
94
|
+
}
|
|
95
|
+
.toast.show { opacity: 1; }
|
|
96
|
+
#drag-overlay {
|
|
97
|
+
position: absolute; inset: 0; cursor: grab; z-index: 2;
|
|
98
|
+
}
|
|
99
|
+
#drag-overlay:active { cursor: grabbing; }
|
|
100
|
+
.video-placeholder {
|
|
101
|
+
display: flex; align-items: center; justify-content: center;
|
|
102
|
+
width: 100%; height: 100%;
|
|
103
|
+
}
|
|
104
|
+
.video-placeholder button {
|
|
105
|
+
font-size: 16px; padding: 12px 28px; border-radius: 8px;
|
|
106
|
+
}
|
package/src/utils/adb.ts
CHANGED
|
@@ -9,38 +9,48 @@ import { verbose } from './verbose.js';
|
|
|
9
9
|
|
|
10
10
|
const CDP_PORT = 9223; // Chrome DevTools Protocol port (Quest browser default)
|
|
11
11
|
|
|
12
|
+
/** Escape a string for safe use in adb shell commands. */
|
|
13
|
+
function shellEscape(s: string): string {
|
|
14
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Global ADB device target. When set, all adb commands use -s <device>. */
|
|
18
|
+
let targetDevice: string | undefined;
|
|
19
|
+
|
|
20
|
+
/** Set the global ADB device target (IP:port or serial). */
|
|
21
|
+
export function setAdbDevice(device: string | undefined): void {
|
|
22
|
+
targetDevice = device;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Get the global ADB device target. */
|
|
26
|
+
export function getAdbDevice(): string | undefined {
|
|
27
|
+
return targetDevice;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Build ADB args with -s <device> prefix when a target device is set. */
|
|
31
|
+
export function adbArgs(...args: string[]): string[] {
|
|
32
|
+
if (targetDevice) {
|
|
33
|
+
return ["-s", targetDevice, ...args];
|
|
34
|
+
}
|
|
35
|
+
return args;
|
|
36
|
+
}
|
|
37
|
+
|
|
12
38
|
/**
|
|
13
39
|
* Get browser process PID
|
|
14
40
|
*/
|
|
15
41
|
async function getBrowserPID(packageName: string): Promise<number | null> {
|
|
16
42
|
try {
|
|
17
|
-
const result = await execCommandFull('adb',
|
|
18
|
-
verbose('getBrowserPID
|
|
19
|
-
if (!result.stdout) return null;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const lines = result.stdout.trim().split('\n');
|
|
25
|
-
for (const line of lines) {
|
|
26
|
-
if (line.includes('grep')) continue; // Skip grep itself
|
|
27
|
-
const parts = line.trim().split(/\s+/);
|
|
28
|
-
if (parts.length >= 2) {
|
|
29
|
-
// Check that the process name is exactly the package (last column)
|
|
30
|
-
const processName = parts[parts.length - 1];
|
|
31
|
-
if (processName === packageName) {
|
|
32
|
-
const pid = parseInt(parts[1], 10);
|
|
33
|
-
verbose('getBrowserPID found PID:', pid, 'for', packageName);
|
|
34
|
-
return pid;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
verbose('getBrowserPID: no exact match for', packageName);
|
|
43
|
+
const result = await execCommandFull('adb', adbArgs('shell', 'pidof', shellEscape(packageName)));
|
|
44
|
+
verbose('getBrowserPID pidof output:', result.stdout?.trim());
|
|
45
|
+
if (!result.stdout?.trim()) return null;
|
|
46
|
+
const pid = parseInt(result.stdout.trim().split(/\s+/)[0], 10);
|
|
47
|
+
if (isNaN(pid)) return null;
|
|
48
|
+
verbose('getBrowserPID found PID:', pid, 'for', packageName);
|
|
49
|
+
return pid;
|
|
39
50
|
} catch (e) {
|
|
40
51
|
verbose('getBrowserPID error:', e);
|
|
41
52
|
return null;
|
|
42
53
|
}
|
|
43
|
-
return null;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
/**
|
|
@@ -55,10 +65,9 @@ async function detectCDPSocket(packageName: string): Promise<string> {
|
|
|
55
65
|
const socketName = `chrome_devtools_remote_${pid}`;
|
|
56
66
|
for (let attempt = 0; attempt < 5; attempt++) {
|
|
57
67
|
try {
|
|
58
|
-
const result = await execCommandFull('adb',
|
|
59
|
-
'shell',
|
|
60
|
-
|
|
61
|
-
]);
|
|
68
|
+
const result = await execCommandFull('adb', adbArgs(
|
|
69
|
+
'shell', 'cat', '/proc/net/unix',
|
|
70
|
+
));
|
|
62
71
|
if (result.stdout.includes(socketName)) {
|
|
63
72
|
verbose('detectCDPSocket: found PID-specific socket:', socketName, `(attempt ${attempt + 1})`);
|
|
64
73
|
return socketName;
|
|
@@ -132,9 +141,24 @@ async function restartADBServer(): Promise<boolean> {
|
|
|
132
141
|
*/
|
|
133
142
|
export async function checkADBDevices(retryCount = 0): Promise<boolean> {
|
|
134
143
|
try {
|
|
144
|
+
const target = getAdbDevice();
|
|
135
145
|
const output = await execCommand('adb', ['devices']);
|
|
136
146
|
const lines = output.trim().split('\n').slice(1); // Skip header
|
|
137
|
-
|
|
147
|
+
let devices = lines.filter(line => line.trim() && !line.includes('List of devices'));
|
|
148
|
+
|
|
149
|
+
// If a target device is configured, check it's in the list
|
|
150
|
+
if (target) {
|
|
151
|
+
const targetOnline = devices.some(line => line.includes(target) && line.includes('device'));
|
|
152
|
+
if (!targetOnline) {
|
|
153
|
+
console.error(`Error: Configured device ${target} not found or offline`);
|
|
154
|
+
console.error('');
|
|
155
|
+
console.error('Check connection: adb connect ' + target);
|
|
156
|
+
console.error('');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
// Filter to just the target device for the count
|
|
160
|
+
devices = devices.filter(line => line.includes(target));
|
|
161
|
+
}
|
|
138
162
|
|
|
139
163
|
if (devices.length === 0) {
|
|
140
164
|
console.error('Error: No ADB devices connected');
|
|
@@ -204,19 +228,19 @@ export async function ensurePortForwarding(
|
|
|
204
228
|
const cdpPort = getCDPPortForSocket(cdpSocket);
|
|
205
229
|
|
|
206
230
|
// Check reverse forwarding (Quest -> Host for dev server)
|
|
207
|
-
const reverseList = await execCommand('adb',
|
|
231
|
+
const reverseList = await execCommand('adb', adbArgs('reverse', '--list'));
|
|
208
232
|
const reverseExists = reverseList.includes(`tcp:${port}`);
|
|
209
233
|
|
|
210
234
|
if (reverseExists) {
|
|
211
235
|
console.log(`ADB reverse port forwarding already set up: Quest:${port} -> Host:${port}`);
|
|
212
236
|
} else {
|
|
213
|
-
await execCommand('adb',
|
|
237
|
+
await execCommand('adb', adbArgs('reverse', `tcp:${port}`, `tcp:${port}`));
|
|
214
238
|
console.log(`ADB reverse port forwarding set up: Quest:${port} -> Host:${port}`);
|
|
215
239
|
}
|
|
216
240
|
|
|
217
241
|
// Check forward forwarding (Host -> Quest for CDP)
|
|
218
242
|
// First check if ADB already has this forwarding set up
|
|
219
|
-
const forwardList = await execCommand('adb',
|
|
243
|
+
const forwardList = await execCommand('adb', adbArgs('forward', '--list'));
|
|
220
244
|
const forwardExists = forwardList.includes(`tcp:${cdpPort}`) && forwardList.includes(cdpSocket);
|
|
221
245
|
|
|
222
246
|
if (forwardExists) {
|
|
@@ -236,7 +260,7 @@ export async function ensurePortForwarding(
|
|
|
236
260
|
process.exit(1);
|
|
237
261
|
}
|
|
238
262
|
|
|
239
|
-
await execCommand('adb',
|
|
263
|
+
await execCommand('adb', adbArgs('forward', `tcp:${cdpPort}`, `localabstract:${cdpSocket}`));
|
|
240
264
|
console.log(`ADB forward port forwarding set up: Host:${cdpPort} -> Quest:${cdpSocket} (CDP)`);
|
|
241
265
|
}
|
|
242
266
|
} catch (error) {
|
|
@@ -250,21 +274,10 @@ export async function ensurePortForwarding(
|
|
|
250
274
|
*/
|
|
251
275
|
export async function isBrowserRunning(browser: string = 'com.oculus.browser'): Promise<boolean> {
|
|
252
276
|
try {
|
|
253
|
-
const result = await execCommandFull('adb',
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
for (const line of lines) {
|
|
258
|
-
if (line.includes('grep')) continue;
|
|
259
|
-
const parts = line.trim().split(/\s+/);
|
|
260
|
-
const processName = parts[parts.length - 1];
|
|
261
|
-
if (processName === browser) {
|
|
262
|
-
verbose('isBrowserRunning:', browser, 'YES (PID', parts[1] + ')');
|
|
263
|
-
return true;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
verbose('isBrowserRunning:', browser, 'NO (zygote/child only)');
|
|
267
|
-
return false;
|
|
277
|
+
const result = await execCommandFull('adb', adbArgs('shell', 'pidof', shellEscape(browser)));
|
|
278
|
+
const running = result.code === 0 && result.stdout.trim().length > 0;
|
|
279
|
+
verbose('isBrowserRunning:', browser, running ? 'YES' : 'NO');
|
|
280
|
+
return running;
|
|
268
281
|
} catch (error) {
|
|
269
282
|
verbose('isBrowserRunning error:', error);
|
|
270
283
|
return false;
|
|
@@ -277,7 +290,7 @@ export async function isBrowserRunning(browser: string = 'com.oculus.browser'):
|
|
|
277
290
|
export async function launchBrowser(url: string, browser: string = 'com.oculus.browser'): Promise<boolean> {
|
|
278
291
|
console.log('Launching browser...');
|
|
279
292
|
try {
|
|
280
|
-
await execCommand('adb',
|
|
293
|
+
await execCommand('adb', adbArgs(
|
|
281
294
|
'shell',
|
|
282
295
|
'am',
|
|
283
296
|
'start',
|
|
@@ -286,7 +299,7 @@ export async function launchBrowser(url: string, browser: string = 'com.oculus.b
|
|
|
286
299
|
'-d',
|
|
287
300
|
url,
|
|
288
301
|
browser
|
|
289
|
-
|
|
302
|
+
));
|
|
290
303
|
console.log(`Browser launched with URL: ${url}`);
|
|
291
304
|
return true;
|
|
292
305
|
} catch (error) {
|
|
@@ -315,7 +328,7 @@ export async function ensureCDPForwarding(
|
|
|
315
328
|
const cdpPort = getCDPPortForSocket(cdpSocket);
|
|
316
329
|
|
|
317
330
|
// Check forward forwarding (Host -> Quest for CDP)
|
|
318
|
-
const forwardList = await execCommand('adb',
|
|
331
|
+
const forwardList = await execCommand('adb', adbArgs('forward', '--list'));
|
|
319
332
|
const forwardExists = forwardList.includes(`tcp:${cdpPort}`) && forwardList.includes(cdpSocket);
|
|
320
333
|
|
|
321
334
|
if (forwardExists) {
|
|
@@ -335,7 +348,7 @@ export async function ensureCDPForwarding(
|
|
|
335
348
|
process.exit(1);
|
|
336
349
|
}
|
|
337
350
|
|
|
338
|
-
await execCommand('adb',
|
|
351
|
+
await execCommand('adb', adbArgs('forward', `tcp:${cdpPort}`, `localabstract:${cdpSocket}`));
|
|
339
352
|
console.log(`ADB forward port forwarding set up: Host:${cdpPort} -> Quest:${cdpSocket} (CDP)`);
|
|
340
353
|
}
|
|
341
354
|
} catch (error) {
|
|
@@ -357,7 +370,7 @@ export async function refreshCDPForwarding(
|
|
|
357
370
|
const cdpPort = getCDPPortForSocket(cdpSocket);
|
|
358
371
|
|
|
359
372
|
// Check if forwarding already points to the correct socket
|
|
360
|
-
const forwardList = await execCommand('adb',
|
|
373
|
+
const forwardList = await execCommand('adb', adbArgs('forward', '--list'));
|
|
361
374
|
verbose('refreshCDPForwarding: detected socket:', cdpSocket, 'port:', cdpPort);
|
|
362
375
|
verbose('refreshCDPForwarding: current forwards:', forwardList.trim());
|
|
363
376
|
|
|
@@ -371,10 +384,10 @@ export async function refreshCDPForwarding(
|
|
|
371
384
|
// Remove existing forwarding on CDP port and re-create with correct socket
|
|
372
385
|
if (forwardList.includes(`tcp:${cdpPort}`)) {
|
|
373
386
|
verbose('refreshCDPForwarding: removing stale forwarding on port', cdpPort);
|
|
374
|
-
await execCommandFull('adb',
|
|
387
|
+
await execCommandFull('adb', adbArgs('forward', '--remove', `tcp:${cdpPort}`));
|
|
375
388
|
}
|
|
376
389
|
|
|
377
|
-
await execCommand('adb',
|
|
390
|
+
await execCommand('adb', adbArgs('forward', `tcp:${cdpPort}`, `localabstract:${cdpSocket}`));
|
|
378
391
|
console.log(`CDP forwarding updated: Host:${cdpPort} -> Quest:${cdpSocket}`);
|
|
379
392
|
} catch (error) {
|
|
380
393
|
// Non-fatal: CDP may still work with existing forwarding
|
|
@@ -387,7 +400,7 @@ export async function refreshCDPForwarding(
|
|
|
387
400
|
* After reboot, user must click notification to allow file access
|
|
388
401
|
*/
|
|
389
402
|
export async function checkUSBFileTransfer(): Promise<void> {
|
|
390
|
-
const result = await execCommandFull('adb',
|
|
403
|
+
const result = await execCommandFull('adb', adbArgs('shell', 'ls', '/sdcard/'));
|
|
391
404
|
|
|
392
405
|
if (result.code !== 0 ||
|
|
393
406
|
result.stdout.includes('Permission denied') ||
|
|
@@ -408,7 +421,7 @@ export async function checkUSBFileTransfer(): Promise<void> {
|
|
|
408
421
|
* Screenshots cannot be taken when the display is off
|
|
409
422
|
*/
|
|
410
423
|
export async function checkQuestAwake(): Promise<void> {
|
|
411
|
-
const result = await execCommandFull('adb',
|
|
424
|
+
const result = await execCommandFull('adb', adbArgs('shell', 'dumpsys', 'power'));
|
|
412
425
|
|
|
413
426
|
if (result.stdout.includes('mWakefulness=Asleep')) {
|
|
414
427
|
console.error('Error: Quest display is off');
|
|
@@ -428,7 +441,7 @@ export interface BatteryInfo {
|
|
|
428
441
|
* Get Quest battery info as structured data
|
|
429
442
|
*/
|
|
430
443
|
export async function getBatteryInfo(): Promise<BatteryInfo> {
|
|
431
|
-
const result = await execCommandFull('adb',
|
|
444
|
+
const result = await execCommandFull('adb', adbArgs('shell', 'dumpsys', 'battery'));
|
|
432
445
|
|
|
433
446
|
if (result.code !== 0) {
|
|
434
447
|
throw new Error('Failed to get battery status');
|