@myerscarpenter/quest-dev 1.2.0 → 1.3.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/logcat.d.ts.map +1 -1
- package/build/commands/logcat.js +8 -6
- package/build/commands/logcat.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 +44 -12
- 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 +19 -0
- package/build/utils/config.d.ts.map +1 -0
- package/build/utils/config.js +59 -0
- package/build/utils/config.js.map +1 -0
- package/build/utils/exec.d.ts.map +1 -1
- package/build/utils/exec.js +0 -2
- package/build/utils/exec.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/battery.ts +3 -3
- package/src/commands/logcat.ts +7 -5
- package/src/commands/stay-awake.ts +184 -96
- package/src/index.ts +49 -14
- package/src/utils/adb.ts +17 -13
- package/src/utils/config.ts +66 -0
- package/src/utils/exec.ts +0 -2
- package/tests/config.test.ts +88 -0
- package/tests/exec.test.ts +3 -3
- 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,7 @@ 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
18
|
|
|
19
19
|
// Read version from package.json
|
|
20
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -155,17 +155,52 @@ cli.command(
|
|
|
155
155
|
// Stay-awake command
|
|
156
156
|
cli.command(
|
|
157
157
|
'stay-awake',
|
|
158
|
-
'Keep Quest
|
|
158
|
+
'Keep Quest awake (disables autosleep, guardian, dialogs)',
|
|
159
159
|
(yargs) => {
|
|
160
|
-
return yargs
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
return yargs
|
|
161
|
+
.option('pin', {
|
|
162
|
+
describe: 'Meta Store PIN (or set in .quest-dev.json / ~/.config/quest-dev/config.json)',
|
|
163
|
+
type: 'string',
|
|
164
|
+
})
|
|
165
|
+
.option('idle-timeout', {
|
|
166
|
+
describe: 'Idle timeout in milliseconds (default: 300000 = 5 minutes, or set idleTimeout in config)',
|
|
167
|
+
type: 'number',
|
|
168
|
+
alias: 'i',
|
|
169
|
+
})
|
|
170
|
+
.option('low-battery', {
|
|
171
|
+
describe: 'Exit when battery drops to this percentage (default: 10, or set lowBattery in config)',
|
|
172
|
+
type: 'number',
|
|
173
|
+
})
|
|
174
|
+
.option('disable', {
|
|
175
|
+
describe: 'Manually restore all test properties and exit',
|
|
176
|
+
type: 'boolean',
|
|
177
|
+
default: false,
|
|
178
|
+
})
|
|
179
|
+
.option('status', {
|
|
180
|
+
describe: 'Show current property values and exit',
|
|
181
|
+
type: 'boolean',
|
|
182
|
+
default: false,
|
|
183
|
+
})
|
|
184
|
+
.option('verbose', {
|
|
185
|
+
describe: 'Print battery level on every check (every 60s)',
|
|
186
|
+
type: 'boolean',
|
|
187
|
+
default: false,
|
|
188
|
+
alias: 'v',
|
|
189
|
+
});
|
|
166
190
|
},
|
|
167
191
|
async (argv) => {
|
|
168
|
-
|
|
192
|
+
if (argv.status) {
|
|
193
|
+
await stayAwakeStatus();
|
|
194
|
+
} else if (argv.disable) {
|
|
195
|
+
await stayAwakeDisable(argv.pin as string | undefined);
|
|
196
|
+
} else {
|
|
197
|
+
await stayAwakeCommand(
|
|
198
|
+
argv.pin as string | undefined,
|
|
199
|
+
argv.idleTimeout as number | undefined,
|
|
200
|
+
argv.lowBattery as number | undefined,
|
|
201
|
+
argv.verbose as boolean,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
169
204
|
}
|
|
170
205
|
);
|
|
171
206
|
|
|
@@ -177,15 +212,15 @@ cli.command(
|
|
|
177
212
|
return yargs
|
|
178
213
|
.option('parent-pid', {
|
|
179
214
|
type: 'number',
|
|
180
|
-
demandOption: true
|
|
215
|
+
demandOption: true,
|
|
181
216
|
})
|
|
182
|
-
.option('
|
|
183
|
-
type: '
|
|
184
|
-
demandOption: true
|
|
217
|
+
.option('pin', {
|
|
218
|
+
type: 'string',
|
|
219
|
+
demandOption: true,
|
|
185
220
|
});
|
|
186
221
|
},
|
|
187
222
|
async (argv) => {
|
|
188
|
-
await stayAwakeWatchdog(argv.parentPid as number, argv.
|
|
223
|
+
await stayAwakeWatchdog(argv.parentPid as number, argv.pin as string);
|
|
189
224
|
}
|
|
190
225
|
);
|
|
191
226
|
|
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
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config file loading for quest-dev
|
|
3
|
+
* Resolves settings from CLI flags → .quest-dev.json → ~/.config/quest-dev/config.json
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
|
|
10
|
+
export interface QuestDevConfig {
|
|
11
|
+
pin?: string;
|
|
12
|
+
idleTimeout?: number;
|
|
13
|
+
lowBattery?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const CONFIG_LOCATIONS = [
|
|
17
|
+
join(process.cwd(), '.quest-dev.json'),
|
|
18
|
+
join(homedir(), '.config', 'quest-dev', 'config.json'),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function tryReadConfig(path: string): QuestDevConfig | null {
|
|
22
|
+
try {
|
|
23
|
+
const content = readFileSync(path, 'utf-8');
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load merged config from all config file locations.
|
|
32
|
+
* First file found wins for each field.
|
|
33
|
+
*/
|
|
34
|
+
export function loadConfig(): QuestDevConfig {
|
|
35
|
+
const merged: QuestDevConfig = {};
|
|
36
|
+
|
|
37
|
+
for (const path of CONFIG_LOCATIONS) {
|
|
38
|
+
const config = tryReadConfig(path);
|
|
39
|
+
if (!config) continue;
|
|
40
|
+
if (merged.pin === undefined && config.pin) merged.pin = config.pin;
|
|
41
|
+
if (merged.idleTimeout === undefined && config.idleTimeout !== undefined) merged.idleTimeout = config.idleTimeout;
|
|
42
|
+
if (merged.lowBattery === undefined && config.lowBattery !== undefined) merged.lowBattery = config.lowBattery;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return merged;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve PIN from CLI flag, then config files
|
|
50
|
+
*/
|
|
51
|
+
export function loadPin(cliPin?: string): string {
|
|
52
|
+
if (cliPin) return cliPin;
|
|
53
|
+
|
|
54
|
+
const config = loadConfig();
|
|
55
|
+
if (config.pin) return config.pin;
|
|
56
|
+
|
|
57
|
+
console.error('Error: No PIN found');
|
|
58
|
+
console.error('');
|
|
59
|
+
console.error('Provide a PIN via one of:');
|
|
60
|
+
console.error(' --pin <pin> CLI flag');
|
|
61
|
+
console.error(' .quest-dev.json { "pin": "1234" }');
|
|
62
|
+
console.error(' ~/.config/quest-dev/config.json { "pin": "1234" }');
|
|
63
|
+
console.error('');
|
|
64
|
+
console.error('The PIN is your Meta Store PIN for the logged-in account.');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
package/src/utils/exec.ts
CHANGED
|
@@ -17,7 +17,6 @@ export function execCommand(command: string, args: string[] = []): Promise<strin
|
|
|
17
17
|
return new Promise((resolve, reject) => {
|
|
18
18
|
const proc = spawn(command, args, {
|
|
19
19
|
stdio: 'pipe',
|
|
20
|
-
shell: true
|
|
21
20
|
});
|
|
22
21
|
|
|
23
22
|
let stdout = '';
|
|
@@ -54,7 +53,6 @@ export function execCommandFull(command: string, args: string[] = []): Promise<E
|
|
|
54
53
|
return new Promise((resolve) => {
|
|
55
54
|
const proc = spawn(command, args, {
|
|
56
55
|
stdio: 'pipe',
|
|
57
|
-
shell: true
|
|
58
56
|
});
|
|
59
57
|
|
|
60
58
|
let stdout = '';
|