@myerscarpenter/quest-dev 1.2.1 → 1.4.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/README.md +65 -1
- package/build/commands/battery.js +3 -3
- package/build/commands/battery.js.map +1 -1
- package/build/commands/stay-awake.d.ts +31 -4
- package/build/commands/stay-awake.d.ts.map +1 -1
- package/build/commands/stay-awake.js +160 -83
- package/build/commands/stay-awake.js.map +1 -1
- package/build/index.js +94 -19
- package/build/index.js.map +1 -1
- package/build/utils/adb.d.ts +10 -3
- package/build/utils/adb.d.ts.map +1 -1
- package/build/utils/adb.js +10 -13
- package/build/utils/adb.js.map +1 -1
- package/build/utils/config.d.ts +24 -0
- package/build/utils/config.d.ts.map +1 -0
- package/build/utils/config.js +82 -0
- package/build/utils/config.js.map +1 -0
- package/docs/tests-with-hardware.md +128 -0
- package/package.json +3 -2
- package/src/commands/battery.ts +3 -3
- package/src/commands/stay-awake.ts +184 -96
- package/src/index.ts +104 -22
- package/src/utils/adb.ts +17 -13
- package/src/utils/config.ts +88 -0
- package/tests/config.test.ts +88 -0
- package/tests/stay-awake.test.ts +71 -0
|
@@ -1,43 +1,100 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quest stay-awake command
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Uses Meta Scriptable Testing API (content://com.oculus.rc) to disable
|
|
4
|
+
* autosleep, guardian, and system dialogs for automated testing.
|
|
5
|
+
*
|
|
6
|
+
* Cleanup is critical: with autosleep disabled, the headset drains battery
|
|
7
|
+
* quickly. A watchdog child process ensures cleanup happens even if the
|
|
8
|
+
* parent is killed (TaskStop, terminal close, claude code exit).
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
|
-
import { checkADBPath } from '../utils/adb.js';
|
|
8
|
-
import {
|
|
11
|
+
import { checkADBPath, getBatteryInfo, formatBatteryInfo } from '../utils/adb.js';
|
|
12
|
+
import { loadPin, loadConfig } from '../utils/config.js';
|
|
13
|
+
import { execCommand, execCommandFull } from '../utils/exec.js';
|
|
9
14
|
import { execSync, spawn, ChildProcess } from 'child_process';
|
|
10
15
|
import * as os from 'os';
|
|
11
16
|
import * as fs from 'fs';
|
|
12
17
|
|
|
18
|
+
export interface TestProperties {
|
|
19
|
+
disable_guardian: boolean;
|
|
20
|
+
disable_dialogs: boolean;
|
|
21
|
+
disable_autosleep: boolean;
|
|
22
|
+
set_proximity_close: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
/**
|
|
14
|
-
*
|
|
26
|
+
* Build ADB args for SET_PROPERTY call
|
|
15
27
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
export function buildSetPropertyArgs(pin: string, enabled: boolean): string[] {
|
|
29
|
+
return [
|
|
30
|
+
'shell', 'content', 'call',
|
|
31
|
+
'--uri', 'content://com.oculus.rc',
|
|
32
|
+
'--method', 'SET_PROPERTY',
|
|
33
|
+
'--extra', `disable_guardian:b:${enabled}`,
|
|
34
|
+
'--extra', `disable_dialogs:b:${enabled}`,
|
|
35
|
+
'--extra', `disable_autosleep:b:${enabled}`,
|
|
36
|
+
'--extra', `set_proximity_close:b:${enabled}`,
|
|
37
|
+
'--extra', `PIN:s:${pin}`,
|
|
38
|
+
];
|
|
19
39
|
}
|
|
20
40
|
|
|
21
41
|
/**
|
|
22
|
-
*
|
|
42
|
+
* Parse GET_PROPERTY Bundle output into structured data
|
|
43
|
+
* Input: "Bundle[{disable_guardian=true, set_proximity_close=true, disable_dialogs=true, disable_autosleep=true}]"
|
|
23
44
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
export function parseTestProperties(output: string): TestProperties {
|
|
46
|
+
const defaults: TestProperties = {
|
|
47
|
+
disable_guardian: false,
|
|
48
|
+
disable_dialogs: false,
|
|
49
|
+
disable_autosleep: false,
|
|
50
|
+
set_proximity_close: false,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const match = output.match(/Bundle\[\{(.+)\}\]/);
|
|
54
|
+
if (!match) return defaults;
|
|
55
|
+
|
|
56
|
+
const pairs = match[1].split(',').map(s => s.trim());
|
|
57
|
+
for (const pair of pairs) {
|
|
58
|
+
const [key, value] = pair.split('=');
|
|
59
|
+
if (key && value && key in defaults) {
|
|
60
|
+
(defaults as any)[key] = value === 'true';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return defaults;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Call SET_PROPERTY to enable or disable test mode
|
|
69
|
+
*/
|
|
70
|
+
async function setTestProperties(pin: string, enabled: boolean): Promise<void> {
|
|
71
|
+
const args = buildSetPropertyArgs(pin, enabled);
|
|
72
|
+
await execCommand('adb', args);
|
|
26
73
|
}
|
|
27
74
|
|
|
28
75
|
/**
|
|
29
|
-
*
|
|
76
|
+
* Call GET_PROPERTY and return parsed test properties
|
|
30
77
|
*/
|
|
31
|
-
async function
|
|
32
|
-
await
|
|
78
|
+
async function getTestProperties(): Promise<TestProperties> {
|
|
79
|
+
const result = await execCommandFull('adb', [
|
|
80
|
+
'shell', 'content', 'call',
|
|
81
|
+
'--uri', 'content://com.oculus.rc',
|
|
82
|
+
'--method', 'GET_PROPERTY',
|
|
83
|
+
]);
|
|
84
|
+
return parseTestProperties(result.stdout);
|
|
33
85
|
}
|
|
34
86
|
|
|
35
87
|
/**
|
|
36
|
-
*
|
|
37
|
-
* Note: automation_disable actually RE-ENABLES normal proximity sensor automation
|
|
88
|
+
* Format test properties for display
|
|
38
89
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
90
|
+
function formatTestProperties(props: TestProperties): string {
|
|
91
|
+
const lines = [
|
|
92
|
+
` Guardian disabled: ${props.disable_guardian}`,
|
|
93
|
+
` Dialogs disabled: ${props.disable_dialogs}`,
|
|
94
|
+
` Autosleep disabled: ${props.disable_autosleep}`,
|
|
95
|
+
` Proximity close: ${props.set_proximity_close}`,
|
|
96
|
+
];
|
|
97
|
+
return lines.join('\n');
|
|
41
98
|
}
|
|
42
99
|
|
|
43
100
|
/**
|
|
@@ -47,35 +104,47 @@ async function wakeScreen(): Promise<void> {
|
|
|
47
104
|
await execCommand('adb', ['shell', 'input', 'keyevent', 'KEYCODE_WAKEUP']);
|
|
48
105
|
}
|
|
49
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Show current test properties status
|
|
109
|
+
*/
|
|
110
|
+
export async function stayAwakeStatus(): Promise<void> {
|
|
111
|
+
checkADBPath();
|
|
112
|
+
const props = await getTestProperties();
|
|
113
|
+
console.log('Scriptable Testing properties:');
|
|
114
|
+
console.log(formatTestProperties(props));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Manually disable test mode (restore all properties)
|
|
119
|
+
*/
|
|
120
|
+
export async function stayAwakeDisable(cliPin?: string): Promise<void> {
|
|
121
|
+
checkADBPath();
|
|
122
|
+
const pin = loadPin(cliPin);
|
|
123
|
+
await setTestProperties(pin, false);
|
|
124
|
+
console.log('Test mode disabled — guardian, dialogs, and autosleep restored');
|
|
125
|
+
}
|
|
126
|
+
|
|
50
127
|
/**
|
|
51
128
|
* Child watchdog process - polls for parent death and cleans up
|
|
52
129
|
*/
|
|
53
|
-
export async function stayAwakeWatchdog(parentPid: number,
|
|
54
|
-
const pollInterval = 5000;
|
|
130
|
+
export async function stayAwakeWatchdog(parentPid: number, pin: string): Promise<void> {
|
|
131
|
+
const pollInterval = 5000;
|
|
55
132
|
|
|
56
133
|
const checkParent = setInterval(() => {
|
|
57
134
|
try {
|
|
58
|
-
// Check if parent process still exists
|
|
59
135
|
process.kill(parentPid, 0);
|
|
60
|
-
// Parent still alive, continue polling
|
|
61
136
|
} catch {
|
|
62
|
-
// Parent is dead - perform cleanup
|
|
63
137
|
console.log('Parent process died, restoring Quest settings...');
|
|
64
138
|
clearInterval(checkParent);
|
|
65
139
|
|
|
66
|
-
// Restore settings synchronously
|
|
67
140
|
try {
|
|
68
|
-
|
|
69
|
-
execSync(`adb
|
|
141
|
+
const args = buildSetPropertyArgs(pin, false);
|
|
142
|
+
execSync(`adb ${args.join(' ')}`, { stdio: 'ignore' });
|
|
70
143
|
|
|
71
|
-
// Cleanup PID file
|
|
72
144
|
const pidFile = `${os.homedir()}/.quest-dev-stay-awake.pid`;
|
|
73
|
-
try {
|
|
74
|
-
fs.unlinkSync(pidFile);
|
|
75
|
-
} catch {}
|
|
145
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
76
146
|
|
|
77
|
-
console.log(
|
|
78
|
-
console.log('Proximity sensor re-enabled');
|
|
147
|
+
console.log('Test mode disabled — guardian, dialogs, and autosleep restored');
|
|
79
148
|
} catch (err) {
|
|
80
149
|
console.error('Failed to restore settings:', (err as Error).message);
|
|
81
150
|
}
|
|
@@ -88,11 +157,15 @@ export async function stayAwakeWatchdog(parentPid: number, originalTimeout: numb
|
|
|
88
157
|
/**
|
|
89
158
|
* Main stay-awake command handler
|
|
90
159
|
*/
|
|
91
|
-
export async function stayAwakeCommand(
|
|
92
|
-
|
|
160
|
+
export async function stayAwakeCommand(
|
|
161
|
+
cliPin?: string,
|
|
162
|
+
cliIdleTimeout?: number,
|
|
163
|
+
cliLowBattery?: number,
|
|
164
|
+
verbose: boolean = false,
|
|
165
|
+
): Promise<void> {
|
|
93
166
|
checkADBPath();
|
|
94
167
|
|
|
95
|
-
// Check devices
|
|
168
|
+
// Check devices
|
|
96
169
|
try {
|
|
97
170
|
const output = await execCommand('adb', ['devices']);
|
|
98
171
|
const lines = output.trim().split('\n').slice(1);
|
|
@@ -107,81 +180,87 @@ export async function stayAwakeCommand(idleTimeout: number = 300000): Promise<vo
|
|
|
107
180
|
process.exit(1);
|
|
108
181
|
}
|
|
109
182
|
|
|
183
|
+
const config = loadConfig();
|
|
184
|
+
const pin = loadPin(cliPin);
|
|
185
|
+
const idleTimeout = cliIdleTimeout ?? config.idleTimeout ?? 300000;
|
|
186
|
+
const lowBattery = cliLowBattery ?? config.lowBattery ?? 10;
|
|
187
|
+
|
|
110
188
|
// PID file management
|
|
111
189
|
const pidFilePath = `${os.homedir()}/.quest-dev-stay-awake.pid`;
|
|
112
190
|
|
|
113
|
-
// Check for existing process
|
|
114
191
|
if (fs.existsSync(pidFilePath)) {
|
|
115
192
|
const existingPid = parseInt(fs.readFileSync(pidFilePath, 'utf-8'));
|
|
116
193
|
try {
|
|
117
|
-
process.kill(existingPid, 0);
|
|
194
|
+
process.kill(existingPid, 0);
|
|
118
195
|
console.error(`Error: stay-awake is already running (PID: ${existingPid})`);
|
|
119
196
|
process.exit(1);
|
|
120
197
|
} catch {
|
|
121
|
-
// Process dead, cleanup stale PID file
|
|
122
198
|
fs.unlinkSync(pidFilePath);
|
|
123
199
|
}
|
|
124
200
|
}
|
|
125
201
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
originalTimeout = await getScreenTimeout();
|
|
131
|
-
console.log(`Original screen timeout: ${originalTimeout}ms (${Math.round(originalTimeout / 1000)}s)`);
|
|
132
|
-
} catch (error) {
|
|
133
|
-
console.error('Failed to get current screen timeout');
|
|
134
|
-
process.exit(1);
|
|
135
|
-
}
|
|
202
|
+
// Show current state
|
|
203
|
+
const beforeProps = await getTestProperties();
|
|
204
|
+
console.log('Current test properties:');
|
|
205
|
+
console.log(formatTestProperties(beforeProps));
|
|
136
206
|
|
|
137
207
|
// Write PID file
|
|
138
208
|
try {
|
|
139
209
|
fs.writeFileSync(pidFilePath, process.pid.toString());
|
|
140
210
|
} catch (error) {
|
|
141
|
-
console.warn('Failed to write PID file
|
|
211
|
+
console.warn('Failed to write PID file');
|
|
142
212
|
}
|
|
143
213
|
|
|
144
|
-
// Spawn child
|
|
214
|
+
// Spawn watchdog child process
|
|
145
215
|
let childProcess: ChildProcess | null = null;
|
|
146
216
|
try {
|
|
147
217
|
childProcess = spawn(process.execPath, [
|
|
148
|
-
process.argv[1],
|
|
218
|
+
process.argv[1],
|
|
149
219
|
'stay-awake-watchdog',
|
|
150
220
|
'--parent-pid', process.pid.toString(),
|
|
151
|
-
'--
|
|
221
|
+
'--pin', pin,
|
|
152
222
|
], {
|
|
153
223
|
detached: true,
|
|
154
|
-
stdio: 'ignore'
|
|
224
|
+
stdio: 'ignore',
|
|
155
225
|
});
|
|
156
|
-
|
|
157
|
-
childProcess.unref(); // Allow parent to exit without waiting for child
|
|
226
|
+
childProcess.unref();
|
|
158
227
|
} catch (error) {
|
|
159
228
|
console.warn('Failed to spawn watchdog child process');
|
|
160
229
|
}
|
|
161
230
|
|
|
162
|
-
//
|
|
231
|
+
// Enable test mode
|
|
232
|
+
try {
|
|
233
|
+
await setTestProperties(pin, true);
|
|
234
|
+
console.log('Test mode enabled — guardian, dialogs, and autosleep disabled');
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error('Failed to enable test mode:', (error as Error).message);
|
|
237
|
+
console.error('Requires Quest OS v44+ and a valid Meta Store PIN.');
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Wake screen
|
|
163
242
|
try {
|
|
164
243
|
await wakeScreen();
|
|
165
244
|
console.log('Quest screen woken up');
|
|
166
|
-
|
|
167
|
-
await disableProximitySensor();
|
|
168
|
-
console.log('Proximity sensor disabled');
|
|
169
245
|
} catch (error) {
|
|
170
|
-
console.error('Failed to wake screen
|
|
246
|
+
console.error('Failed to wake screen:', (error as Error).message);
|
|
171
247
|
}
|
|
172
248
|
|
|
173
|
-
//
|
|
174
|
-
|
|
249
|
+
// Battery monitoring state
|
|
250
|
+
let lastReportedBucket = -1; // Track 5% boundary crossings
|
|
251
|
+
|
|
252
|
+
// Initial battery check
|
|
175
253
|
try {
|
|
176
|
-
await
|
|
177
|
-
console.log(`
|
|
178
|
-
|
|
254
|
+
const battery = await getBatteryInfo();
|
|
255
|
+
console.log(`Battery: ${formatBatteryInfo(battery)}`);
|
|
256
|
+
lastReportedBucket = Math.floor(battery.level / 5) * 5;
|
|
179
257
|
} catch (error) {
|
|
180
|
-
console.
|
|
181
|
-
process.exit(1);
|
|
258
|
+
console.warn('Failed to read battery status');
|
|
182
259
|
}
|
|
183
260
|
|
|
184
|
-
|
|
261
|
+
console.log(`Quest will stay awake (idle timeout: ${Math.round(idleTimeout / 1000)}s, low battery exit: ${lowBattery}%). Press Ctrl-C to restore.`);
|
|
262
|
+
|
|
263
|
+
// Idle timer
|
|
185
264
|
let idleTimerHandle: NodeJS.Timeout | null = null;
|
|
186
265
|
let cleanupInProgress = false;
|
|
187
266
|
|
|
@@ -193,62 +272,71 @@ export async function stayAwakeCommand(idleTimeout: number = 300000): Promise<vo
|
|
|
193
272
|
}, idleTimeout);
|
|
194
273
|
};
|
|
195
274
|
|
|
196
|
-
//
|
|
275
|
+
// Cleanup handler
|
|
197
276
|
const cleanup = () => {
|
|
198
|
-
if (cleanupInProgress) return;
|
|
277
|
+
if (cleanupInProgress) return;
|
|
199
278
|
cleanupInProgress = true;
|
|
200
279
|
|
|
201
|
-
// Clear idle timer
|
|
202
280
|
if (idleTimerHandle) clearTimeout(idleTimerHandle);
|
|
281
|
+
if (batteryInterval) clearInterval(batteryInterval);
|
|
203
282
|
|
|
204
|
-
// Kill child watchdog
|
|
205
283
|
if (childProcess) {
|
|
206
|
-
try {
|
|
207
|
-
childProcess.kill();
|
|
208
|
-
} catch {}
|
|
284
|
+
try { childProcess.kill(); } catch {}
|
|
209
285
|
}
|
|
210
286
|
|
|
211
|
-
console.log('\nRestoring
|
|
287
|
+
console.log('\nRestoring settings...');
|
|
212
288
|
try {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Restore Quest settings
|
|
219
|
-
execSync(`adb shell settings put system screen_off_timeout ${originalTimeout}`, { stdio: 'ignore' });
|
|
220
|
-
execSync(`adb shell am broadcast -a com.oculus.vrpowermanager.automation_disable`, { stdio: 'ignore' });
|
|
221
|
-
console.log(`Screen timeout restored to ${originalTimeout}ms (${Math.round(originalTimeout / 1000)}s)`);
|
|
222
|
-
console.log(`Proximity sensor re-enabled`);
|
|
289
|
+
try { fs.unlinkSync(pidFilePath); } catch {}
|
|
290
|
+
|
|
291
|
+
const args = buildSetPropertyArgs(pin, false);
|
|
292
|
+
execSync(`adb ${args.join(' ')}`, { stdio: 'ignore' });
|
|
293
|
+
console.log('Test mode disabled — guardian, dialogs, and autosleep restored');
|
|
223
294
|
} catch (error) {
|
|
224
295
|
console.error('Failed to restore settings:', (error as Error).message);
|
|
225
296
|
}
|
|
226
297
|
process.exit(0);
|
|
227
298
|
};
|
|
228
299
|
|
|
229
|
-
//
|
|
300
|
+
// Signal handlers
|
|
230
301
|
process.on('SIGINT', cleanup);
|
|
231
302
|
process.on('SIGTERM', cleanup);
|
|
232
303
|
process.on('SIGHUP', cleanup);
|
|
233
304
|
|
|
234
|
-
//
|
|
305
|
+
// Activity reset via SIGUSR1
|
|
235
306
|
process.on('SIGUSR1', () => {
|
|
236
|
-
|
|
307
|
+
const now = new Date().toLocaleTimeString();
|
|
308
|
+
console.log(`[${now}] Activity detected, resetting idle timer`);
|
|
237
309
|
resetIdleTimer();
|
|
238
310
|
});
|
|
239
311
|
|
|
240
312
|
// Start idle timer
|
|
241
313
|
resetIdleTimer();
|
|
242
314
|
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
315
|
+
// Battery monitoring loop (every 60s)
|
|
316
|
+
const batteryInterval = setInterval(async () => {
|
|
317
|
+
try {
|
|
318
|
+
const battery = await getBatteryInfo();
|
|
319
|
+
const currentBucket = Math.floor(battery.level / 5) * 5;
|
|
320
|
+
|
|
321
|
+
if (verbose) {
|
|
322
|
+
console.log(`Battery: ${formatBatteryInfo(battery)}`);
|
|
323
|
+
} else if (currentBucket !== lastReportedBucket) {
|
|
324
|
+
console.log(`Battery: ${formatBatteryInfo(battery)}`);
|
|
325
|
+
}
|
|
326
|
+
lastReportedBucket = currentBucket;
|
|
327
|
+
|
|
328
|
+
if (battery.level <= lowBattery && battery.state === 'not charging') {
|
|
329
|
+
console.log(`\nBattery critically low (${battery.level}%), exiting to preserve battery...`);
|
|
330
|
+
cleanup();
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
// Ignore battery check failures (device might be briefly unavailable)
|
|
334
|
+
}
|
|
335
|
+
}, 60000);
|
|
248
336
|
|
|
249
|
-
//
|
|
337
|
+
// Keep process alive
|
|
338
|
+
console.log('Keeping Quest awake...');
|
|
250
339
|
await new Promise<void>((resolve) => {
|
|
251
|
-
// This will only resolve when cleanup is called
|
|
252
340
|
process.on('exit', () => resolve());
|
|
253
341
|
});
|
|
254
342
|
}
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,8 @@ import { screenshotCommand } from './commands/screenshot.js';
|
|
|
14
14
|
import { openCommand } from './commands/open.js';
|
|
15
15
|
import { startCommand, stopCommand, statusCommand, tailCommand } from './commands/logcat.js';
|
|
16
16
|
import { batteryCommand } from './commands/battery.js';
|
|
17
|
-
import { stayAwakeCommand, stayAwakeWatchdog } from './commands/stay-awake.js';
|
|
17
|
+
import { stayAwakeCommand, stayAwakeWatchdog, stayAwakeStatus, stayAwakeDisable } from './commands/stay-awake.js';
|
|
18
|
+
import { saveConfig, loadConfig } from './utils/config.js';
|
|
18
19
|
|
|
19
20
|
// Read version from package.json
|
|
20
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -28,16 +29,11 @@ const cli = yargs(hideBin(process.argv))
|
|
|
28
29
|
.scriptName('quest-dev')
|
|
29
30
|
.version(version)
|
|
30
31
|
.usage('Usage: $0 <command> [options]')
|
|
31
|
-
.demandCommand(1, '
|
|
32
|
+
.demandCommand(1, '')
|
|
32
33
|
.strict()
|
|
33
34
|
.fail((msg, err, yargs) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
if (err) {
|
|
38
|
-
console.error(err.message);
|
|
39
|
-
}
|
|
40
|
-
console.error('Run "quest-dev --help" for usage information.');
|
|
35
|
+
yargs.showHelp();
|
|
36
|
+
if (err) console.error(err.message);
|
|
41
37
|
process.exit(1);
|
|
42
38
|
})
|
|
43
39
|
.help()
|
|
@@ -155,17 +151,103 @@ cli.command(
|
|
|
155
151
|
// Stay-awake command
|
|
156
152
|
cli.command(
|
|
157
153
|
'stay-awake',
|
|
158
|
-
'Keep Quest
|
|
154
|
+
'Keep Quest awake (disables autosleep, guardian, dialogs)',
|
|
159
155
|
(yargs) => {
|
|
160
|
-
return yargs
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
156
|
+
return yargs
|
|
157
|
+
.option('pin', {
|
|
158
|
+
describe: 'Meta Store PIN (or save with: quest-dev config --pin)',
|
|
159
|
+
type: 'string',
|
|
160
|
+
})
|
|
161
|
+
.option('idle-timeout', {
|
|
162
|
+
describe: 'Idle timeout in milliseconds (default: 300000 = 5 minutes, or save with: quest-dev config)',
|
|
163
|
+
type: 'number',
|
|
164
|
+
alias: 'i',
|
|
165
|
+
})
|
|
166
|
+
.option('low-battery', {
|
|
167
|
+
describe: 'Exit when battery drops to this percentage (default: 10, or save with: quest-dev config)',
|
|
168
|
+
type: 'number',
|
|
169
|
+
})
|
|
170
|
+
.option('disable', {
|
|
171
|
+
describe: 'Manually restore all test properties and exit',
|
|
172
|
+
type: 'boolean',
|
|
173
|
+
default: false,
|
|
174
|
+
})
|
|
175
|
+
.option('status', {
|
|
176
|
+
describe: 'Show current property values and exit',
|
|
177
|
+
type: 'boolean',
|
|
178
|
+
default: false,
|
|
179
|
+
})
|
|
180
|
+
.option('verbose', {
|
|
181
|
+
describe: 'Print battery level on every check (every 60s)',
|
|
182
|
+
type: 'boolean',
|
|
183
|
+
default: false,
|
|
184
|
+
alias: 'v',
|
|
185
|
+
});
|
|
166
186
|
},
|
|
167
187
|
async (argv) => {
|
|
168
|
-
|
|
188
|
+
if (argv.status) {
|
|
189
|
+
await stayAwakeStatus();
|
|
190
|
+
} else if (argv.disable) {
|
|
191
|
+
await stayAwakeDisable(argv.pin as string | undefined);
|
|
192
|
+
} else {
|
|
193
|
+
await stayAwakeCommand(
|
|
194
|
+
argv.pin as string | undefined,
|
|
195
|
+
argv.idleTimeout as number | undefined,
|
|
196
|
+
argv.lowBattery as number | undefined,
|
|
197
|
+
argv.verbose as boolean,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Config command
|
|
204
|
+
cli.command(
|
|
205
|
+
'config',
|
|
206
|
+
'Save default settings for quest-dev commands',
|
|
207
|
+
(yargs) => {
|
|
208
|
+
return yargs
|
|
209
|
+
.option('pin', {
|
|
210
|
+
describe: 'Meta Store PIN',
|
|
211
|
+
type: 'string',
|
|
212
|
+
})
|
|
213
|
+
.option('idle-timeout', {
|
|
214
|
+
describe: 'Idle timeout in milliseconds for stay-awake',
|
|
215
|
+
type: 'number',
|
|
216
|
+
})
|
|
217
|
+
.option('low-battery', {
|
|
218
|
+
describe: 'Exit stay-awake when battery drops to this percentage',
|
|
219
|
+
type: 'number',
|
|
220
|
+
})
|
|
221
|
+
.option('show', {
|
|
222
|
+
describe: 'Show current config and exit',
|
|
223
|
+
type: 'boolean',
|
|
224
|
+
default: false,
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
(argv) => {
|
|
228
|
+
if (argv.show) {
|
|
229
|
+
const config = loadConfig();
|
|
230
|
+
if (Object.keys(config).length === 0) {
|
|
231
|
+
console.log('No config found.');
|
|
232
|
+
} else {
|
|
233
|
+
console.log(JSON.stringify(config, null, 2));
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const values: Record<string, unknown> = {};
|
|
239
|
+
if (argv.pin !== undefined) values.pin = argv.pin;
|
|
240
|
+
if (argv.idleTimeout !== undefined) values.idleTimeout = argv.idleTimeout;
|
|
241
|
+
if (argv.lowBattery !== undefined) values.lowBattery = argv.lowBattery;
|
|
242
|
+
|
|
243
|
+
if (Object.keys(values).length === 0) {
|
|
244
|
+
console.error('No config values provided. Use --pin, --idle-timeout, or --low-battery.');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
saveConfig(values as any);
|
|
249
|
+
console.log('Config saved:');
|
|
250
|
+
console.log(JSON.stringify(values, null, 2));
|
|
169
251
|
}
|
|
170
252
|
);
|
|
171
253
|
|
|
@@ -177,15 +259,15 @@ cli.command(
|
|
|
177
259
|
return yargs
|
|
178
260
|
.option('parent-pid', {
|
|
179
261
|
type: 'number',
|
|
180
|
-
demandOption: true
|
|
262
|
+
demandOption: true,
|
|
181
263
|
})
|
|
182
|
-
.option('
|
|
183
|
-
type: '
|
|
184
|
-
demandOption: true
|
|
264
|
+
.option('pin', {
|
|
265
|
+
type: 'string',
|
|
266
|
+
demandOption: true,
|
|
185
267
|
});
|
|
186
268
|
},
|
|
187
269
|
async (argv) => {
|
|
188
|
-
await stayAwakeWatchdog(argv.parentPid as number, argv.
|
|
270
|
+
await stayAwakeWatchdog(argv.parentPid as number, argv.pin as string);
|
|
189
271
|
}
|
|
190
272
|
);
|
|
191
273
|
|
package/src/utils/adb.ts
CHANGED
|
@@ -345,18 +345,21 @@ export async function checkQuestAwake(): Promise<void> {
|
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
export interface BatteryInfo {
|
|
349
|
+
level: number;
|
|
350
|
+
state: 'fast charging' | 'charging' | 'not charging';
|
|
351
|
+
}
|
|
352
|
+
|
|
348
353
|
/**
|
|
349
|
-
* Get Quest battery
|
|
350
|
-
* Returns percentage and charging state in one line
|
|
354
|
+
* Get Quest battery info as structured data
|
|
351
355
|
*/
|
|
352
|
-
export async function
|
|
356
|
+
export async function getBatteryInfo(): Promise<BatteryInfo> {
|
|
353
357
|
const result = await execCommandFull('adb', ['shell', 'dumpsys', 'battery']);
|
|
354
358
|
|
|
355
359
|
if (result.code !== 0) {
|
|
356
360
|
throw new Error('Failed to get battery status');
|
|
357
361
|
}
|
|
358
362
|
|
|
359
|
-
// Parse battery info
|
|
360
363
|
let level = 0;
|
|
361
364
|
let acPowered = false;
|
|
362
365
|
let usbPowered = false;
|
|
@@ -376,18 +379,19 @@ export async function getBatteryStatus(): Promise<string> {
|
|
|
376
379
|
}
|
|
377
380
|
}
|
|
378
381
|
|
|
379
|
-
|
|
380
|
-
let state: string;
|
|
382
|
+
let state: BatteryInfo['state'];
|
|
381
383
|
if (acPowered || usbPowered) {
|
|
382
|
-
|
|
383
|
-
if (maxChargingCurrent > 2000000) {
|
|
384
|
-
state = 'fast charging';
|
|
385
|
-
} else {
|
|
386
|
-
state = 'charging';
|
|
387
|
-
}
|
|
384
|
+
state = maxChargingCurrent > 2000000 ? 'fast charging' : 'charging';
|
|
388
385
|
} else {
|
|
389
386
|
state = 'not charging';
|
|
390
387
|
}
|
|
391
388
|
|
|
392
|
-
return
|
|
389
|
+
return { level, state };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Format battery info as a human-readable string
|
|
394
|
+
*/
|
|
395
|
+
export function formatBatteryInfo(info: BatteryInfo): string {
|
|
396
|
+
return `${info.level}% ${info.state}`;
|
|
393
397
|
}
|