@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
package/src/commands/logcat.ts
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { resolve, join } from 'path';
|
|
10
|
-
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, symlinkSync, statSync, readlinkSync, openSync } from 'fs';
|
|
10
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, symlinkSync, statSync, readlinkSync, openSync, closeSync } from 'fs';
|
|
11
11
|
import { spawn } from 'child_process';
|
|
12
|
-
import { checkADBPath, checkADBDevices } from '../utils/adb.js';
|
|
12
|
+
import { checkADBPath, checkADBDevices, adbArgs } from '../utils/adb.js';
|
|
13
13
|
import { execCommand, execCommandFull } from '../utils/exec.js';
|
|
14
14
|
|
|
15
|
-
const LOG_DIR = process.env.LOG_DIR || 'logs/logcat';
|
|
15
|
+
const LOG_DIR = resolve(process.env.LOG_DIR || 'logs/logcat');
|
|
16
16
|
const PID_FILE = join(LOG_DIR, '.logcat_pid');
|
|
17
17
|
const LOGFILE_LINK = join(LOG_DIR, 'latest.txt');
|
|
18
18
|
|
|
@@ -143,7 +143,7 @@ export async function startCommand(filter?: string): Promise<void> {
|
|
|
143
143
|
|
|
144
144
|
// Clear the buffer first - critical for Quest
|
|
145
145
|
try {
|
|
146
|
-
await execCommand('adb',
|
|
146
|
+
await execCommand('adb', adbArgs('logcat', '-c'));
|
|
147
147
|
console.log('Ring buffer cleared.');
|
|
148
148
|
} catch (error) {
|
|
149
149
|
console.error('Failed to clear ring buffer:', (error as Error).message);
|
|
@@ -155,7 +155,7 @@ export async function startCommand(filter?: string): Promise<void> {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// Start background logcat process
|
|
158
|
-
const args =
|
|
158
|
+
const args = adbArgs('logcat', '-v', 'threadtime');
|
|
159
159
|
if (filter) {
|
|
160
160
|
args.push(filter);
|
|
161
161
|
}
|
|
@@ -167,6 +167,7 @@ export async function startCommand(filter?: string): Promise<void> {
|
|
|
167
167
|
stdio: ['ignore', fd, fd],
|
|
168
168
|
detached: true
|
|
169
169
|
});
|
|
170
|
+
closeSync(fd); // child process owns the fd now
|
|
170
171
|
|
|
171
172
|
// Unref so parent can exit immediately
|
|
172
173
|
proc.unref();
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { resolve, join } from 'path';
|
|
7
7
|
import { existsSync, statSync } from 'fs';
|
|
8
|
-
import { checkADBPath, checkADBDevices, checkUSBFileTransfer, checkQuestAwake } from '../utils/adb.js';
|
|
8
|
+
import { checkADBPath, checkADBDevices, checkUSBFileTransfer, checkQuestAwake, setAdbDevice, adbArgs } from '../utils/adb.js';
|
|
9
9
|
import { execCommand, execCommandFull } from '../utils/exec.js';
|
|
10
|
+
import { loadConfig } from '../utils/config.js';
|
|
10
11
|
import { generateScreenshotFilename } from '../utils/filename.js';
|
|
12
|
+
import { addJpegFileComment } from '../utils/jpeg-comment.js';
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Validate directory exists and is writable
|
|
@@ -36,7 +38,7 @@ function validateDirectory(dirPath: string): void {
|
|
|
36
38
|
*/
|
|
37
39
|
async function triggerScreenshot(): Promise<boolean> {
|
|
38
40
|
try {
|
|
39
|
-
await execCommand('adb',
|
|
41
|
+
await execCommand('adb', adbArgs(
|
|
40
42
|
'shell',
|
|
41
43
|
'am',
|
|
42
44
|
'startservice',
|
|
@@ -44,7 +46,7 @@ async function triggerScreenshot(): Promise<boolean> {
|
|
|
44
46
|
'com.oculus.metacam/.capture.CaptureService',
|
|
45
47
|
'-a',
|
|
46
48
|
'TAKE_SCREENSHOT'
|
|
47
|
-
|
|
49
|
+
));
|
|
48
50
|
console.log('Screenshot service triggered');
|
|
49
51
|
return true;
|
|
50
52
|
} catch (error) {
|
|
@@ -58,7 +60,7 @@ async function triggerScreenshot(): Promise<boolean> {
|
|
|
58
60
|
*/
|
|
59
61
|
async function getMostRecentScreenshot(): Promise<string | null> {
|
|
60
62
|
try {
|
|
61
|
-
const output = await execCommand('adb',
|
|
63
|
+
const output = await execCommand('adb', adbArgs('shell', 'ls', '-t', '/sdcard/Oculus/Screenshots/'));
|
|
62
64
|
const files = output.split('\n').filter(line => line.trim() && line.endsWith('.jpg'));
|
|
63
65
|
|
|
64
66
|
if (files.length === 0) {
|
|
@@ -82,7 +84,7 @@ async function isJpegComplete(filename: string): Promise<boolean> {
|
|
|
82
84
|
const { spawn } = await import('child_process');
|
|
83
85
|
|
|
84
86
|
return new Promise((resolve) => {
|
|
85
|
-
const proc = spawn('adb',
|
|
87
|
+
const proc = spawn('adb', adbArgs('exec-out', 'tail', '-c', '2', remotePath));
|
|
86
88
|
const chunks: Buffer[] = [];
|
|
87
89
|
|
|
88
90
|
proc.stdout.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
@@ -108,7 +110,7 @@ async function isJpegComplete(filename: string): Promise<boolean> {
|
|
|
108
110
|
async function pullScreenshot(filename: string, outputPath: string): Promise<boolean> {
|
|
109
111
|
try {
|
|
110
112
|
const remotePath = `/sdcard/Oculus/Screenshots/${filename}`;
|
|
111
|
-
await execCommand('adb',
|
|
113
|
+
await execCommand('adb', adbArgs('pull', remotePath, outputPath));
|
|
112
114
|
console.log(`Screenshot saved to: ${outputPath}`);
|
|
113
115
|
return true;
|
|
114
116
|
} catch (error) {
|
|
@@ -122,7 +124,7 @@ async function pullScreenshot(filename: string, outputPath: string): Promise<boo
|
|
|
122
124
|
*/
|
|
123
125
|
async function deleteRemoteScreenshot(filename: string): Promise<void> {
|
|
124
126
|
const remotePath = `/sdcard/Oculus/Screenshots/${filename}`;
|
|
125
|
-
const result = await execCommandFull('adb',
|
|
127
|
+
const result = await execCommandFull('adb', adbArgs('shell', 'rm', remotePath));
|
|
126
128
|
if (result.code !== 0) {
|
|
127
129
|
console.warn(`Warning: Failed to delete screenshot from Quest: ${filename}`);
|
|
128
130
|
} else {
|
|
@@ -133,11 +135,9 @@ async function deleteRemoteScreenshot(filename: string): Promise<void> {
|
|
|
133
135
|
/**
|
|
134
136
|
* Add caption to JPEG COM metadata
|
|
135
137
|
*/
|
|
136
|
-
|
|
138
|
+
function addJpegMetadata(filePath: string, caption: string): boolean {
|
|
137
139
|
try {
|
|
138
|
-
|
|
139
|
-
await exiftool.write(filePath, { Comment: caption });
|
|
140
|
-
await exiftool.end();
|
|
140
|
+
addJpegFileComment(filePath, caption);
|
|
141
141
|
console.log(`Caption added: "${caption}"`);
|
|
142
142
|
return true;
|
|
143
143
|
} catch (error) {
|
|
@@ -158,9 +158,15 @@ export async function screenshotCommand(directoryPath: string, caption: string |
|
|
|
158
158
|
validateDirectory(resolvedDir);
|
|
159
159
|
|
|
160
160
|
// Generate filename
|
|
161
|
-
const localFilename = generateScreenshotFilename();
|
|
161
|
+
const localFilename = generateScreenshotFilename(new Date(), caption);
|
|
162
162
|
const outputPath = join(resolvedDir, localFilename);
|
|
163
163
|
|
|
164
|
+
// Load device config so -s <device> targets the right Quest
|
|
165
|
+
const config = loadConfig();
|
|
166
|
+
if (config.device) {
|
|
167
|
+
setAdbDevice(config.device);
|
|
168
|
+
}
|
|
169
|
+
|
|
164
170
|
// Check prerequisites
|
|
165
171
|
checkADBPath();
|
|
166
172
|
await checkADBDevices();
|
|
@@ -215,7 +221,7 @@ export async function screenshotCommand(directoryPath: string, caption: string |
|
|
|
215
221
|
|
|
216
222
|
// Add metadata (non-fatal, only if caption provided)
|
|
217
223
|
if (caption) {
|
|
218
|
-
|
|
224
|
+
addJpegMetadata(outputPath, caption);
|
|
219
225
|
}
|
|
220
226
|
|
|
221
227
|
// Delete from Quest after successful pull
|
|
@@ -8,100 +8,29 @@
|
|
|
8
8
|
* parent is killed (TaskStop, terminal close, claude code exit).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { checkADBPath, getBatteryInfo, formatBatteryInfo } from '../utils/adb.js';
|
|
11
|
+
import { checkADBPath, getBatteryInfo, formatBatteryInfo, adbArgs } from '../utils/adb.js';
|
|
12
12
|
import { loadPin, loadConfig } from '../utils/config.js';
|
|
13
|
-
import { execCommand
|
|
14
|
-
import {
|
|
13
|
+
import { execCommand } from '../utils/exec.js';
|
|
14
|
+
import { execFileSync, spawn, ChildProcess } from 'child_process';
|
|
15
15
|
import * as os from 'os';
|
|
16
16
|
import * as fs from 'fs';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
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}]"
|
|
44
|
-
*/
|
|
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);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Call GET_PROPERTY and return parsed test properties
|
|
77
|
-
*/
|
|
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);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Format test properties for display
|
|
89
|
-
*/
|
|
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');
|
|
98
|
-
}
|
|
17
|
+
import {
|
|
18
|
+
type TestProperties,
|
|
19
|
+
buildSetPropertyArgs,
|
|
20
|
+
parseTestProperties,
|
|
21
|
+
setTestProperties,
|
|
22
|
+
getTestProperties,
|
|
23
|
+
formatTestProperties,
|
|
24
|
+
} from '../utils/test-properties.js';
|
|
25
|
+
|
|
26
|
+
// Re-export for backward compatibility with tests
|
|
27
|
+
export { type TestProperties, buildSetPropertyArgs, parseTestProperties };
|
|
99
28
|
|
|
100
29
|
/**
|
|
101
30
|
* Wake the Quest screen
|
|
102
31
|
*/
|
|
103
32
|
async function wakeScreen(): Promise<void> {
|
|
104
|
-
await execCommand('adb',
|
|
33
|
+
await execCommand('adb', adbArgs('shell', 'input', 'keyevent', 'KEYCODE_WAKEUP'));
|
|
105
34
|
}
|
|
106
35
|
|
|
107
36
|
/**
|
|
@@ -121,7 +50,9 @@ export async function stayAwakeDisable(cliPin?: string): Promise<void> {
|
|
|
121
50
|
checkADBPath();
|
|
122
51
|
const pin = loadPin(cliPin);
|
|
123
52
|
await setTestProperties(pin, false);
|
|
124
|
-
|
|
53
|
+
const props = await getTestProperties();
|
|
54
|
+
console.log('Test mode disabled:');
|
|
55
|
+
console.log(formatTestProperties(props));
|
|
125
56
|
}
|
|
126
57
|
|
|
127
58
|
/**
|
|
@@ -138,8 +69,8 @@ export async function stayAwakeWatchdog(parentPid: number, pin: string): Promise
|
|
|
138
69
|
clearInterval(checkParent);
|
|
139
70
|
|
|
140
71
|
try {
|
|
141
|
-
const args = buildSetPropertyArgs(pin, false);
|
|
142
|
-
|
|
72
|
+
const args = adbArgs(...buildSetPropertyArgs(pin, false));
|
|
73
|
+
execFileSync('adb', args, { stdio: 'ignore' });
|
|
143
74
|
|
|
144
75
|
const pidFile = `${os.homedir()}/.quest-dev-stay-awake.pid`;
|
|
145
76
|
try { fs.unlinkSync(pidFile); } catch {}
|
|
@@ -288,8 +219,8 @@ export async function stayAwakeCommand(
|
|
|
288
219
|
try {
|
|
289
220
|
try { fs.unlinkSync(pidFilePath); } catch {}
|
|
290
221
|
|
|
291
|
-
const args = buildSetPropertyArgs(pin, false);
|
|
292
|
-
|
|
222
|
+
const args = adbArgs(...buildSetPropertyArgs(pin, false));
|
|
223
|
+
execFileSync('adb', args, { stdio: 'ignore' });
|
|
293
224
|
console.log('Test mode disabled — guardian, dialogs, and autosleep restored');
|
|
294
225
|
} catch (error) {
|
|
295
226
|
console.error('Failed to restore settings:', (error as Error).message);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare module "adbkit-apkreader" {
|
|
2
|
+
interface Manifest {
|
|
3
|
+
package: string;
|
|
4
|
+
versionCode: number;
|
|
5
|
+
versionName: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class ApkReader {
|
|
9
|
+
static open(path: string): Promise<ApkReader>;
|
|
10
|
+
readManifest(): Promise<Manifest>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default ApkReader;
|
|
14
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CastManager: lazy-loaded manager for cast sessions within the daemon.
|
|
3
|
+
* Wraps CastSession with start/stop lifecycle and SSE broadcasting.
|
|
4
|
+
* Server state is the single source of truth — pushed to clients via SSE.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
import type { ServerResponse } from "node:http";
|
|
9
|
+
import { checkADBPath, getAdbDevice } from "../utils/adb.js";
|
|
10
|
+
import { execCommand } from "../utils/exec.js";
|
|
11
|
+
import { verbose } from "../utils/verbose.js";
|
|
12
|
+
import { CastSession } from "../cast/session.js";
|
|
13
|
+
import { ensureCastingInstalled } from "../utils/casting-apk.js";
|
|
14
|
+
import { resolveResolution } from "@myerscarpenter/cast2-protocol";
|
|
15
|
+
|
|
16
|
+
export interface CastStartOptions {
|
|
17
|
+
listenPort?: number;
|
|
18
|
+
resolution?: string;
|
|
19
|
+
width?: number;
|
|
20
|
+
height?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function round4(n: number): number {
|
|
24
|
+
return Math.round(n * 10000) / 10000;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function round1(n: number): number {
|
|
28
|
+
return Math.round(n * 10) / 10;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function deg(rad: number): number {
|
|
32
|
+
return (rad * 180) / Math.PI;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class CastManager extends EventEmitter {
|
|
36
|
+
private session: CastSession | null = null;
|
|
37
|
+
private sseClients = new Set<ServerResponse>();
|
|
38
|
+
private questIp: string | null = null;
|
|
39
|
+
private configuredDevice: string | undefined;
|
|
40
|
+
private statsInterval: ReturnType<typeof setInterval> | null = null;
|
|
41
|
+
private starting = false;
|
|
42
|
+
|
|
43
|
+
constructor(device?: string) {
|
|
44
|
+
super();
|
|
45
|
+
this.configuredDevice = device;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get isActive(): boolean {
|
|
49
|
+
return this.session !== null && this.session.connected;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getSession(): CastSession | null {
|
|
53
|
+
return this.session;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build full status snapshot from current session state. */
|
|
57
|
+
getStatus(): Record<string, unknown> {
|
|
58
|
+
const session = this.session;
|
|
59
|
+
if (!session) {
|
|
60
|
+
return { connected: false, running: false };
|
|
61
|
+
}
|
|
62
|
+
const p = session.pose;
|
|
63
|
+
return {
|
|
64
|
+
connected: session.connected,
|
|
65
|
+
running: session.running,
|
|
66
|
+
width: session.width,
|
|
67
|
+
height: session.height,
|
|
68
|
+
frame_count: session.frameCount,
|
|
69
|
+
bytes: session.byteCount,
|
|
70
|
+
fps: session.fps,
|
|
71
|
+
elapsed: session.elapsedSeconds,
|
|
72
|
+
has_frame: session.getScreenshot() !== null,
|
|
73
|
+
eye: session.eye === 0 ? "right" : session.eye === 2 ? "stereo" : "left",
|
|
74
|
+
pose_loop: session.poseLoopActive,
|
|
75
|
+
pose: {
|
|
76
|
+
x: round4(p.x),
|
|
77
|
+
y: round4(p.y),
|
|
78
|
+
z: round4(p.z),
|
|
79
|
+
yaw: round4(p.yaw),
|
|
80
|
+
pitch: round4(p.pitch),
|
|
81
|
+
yaw_deg: round1(deg(p.yaw)),
|
|
82
|
+
pitch_deg: round1(deg(p.pitch)),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async start(opts: CastStartOptions = {}): Promise<void> {
|
|
88
|
+
if (this.session?.connected) {
|
|
89
|
+
return; // Already active
|
|
90
|
+
}
|
|
91
|
+
if (this.starting) {
|
|
92
|
+
throw new Error("Cast start already in progress");
|
|
93
|
+
}
|
|
94
|
+
this.starting = true;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Stop any lingering session
|
|
98
|
+
if (this.session) {
|
|
99
|
+
await this.stopSession();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
checkADBPath();
|
|
103
|
+
|
|
104
|
+
// Check for connected devices
|
|
105
|
+
const output = await execCommand("adb", ["devices"]);
|
|
106
|
+
const lines = output.trim().split("\n").slice(1);
|
|
107
|
+
const devices = lines.filter(
|
|
108
|
+
(line) => line.trim() && !line.includes("List of devices"),
|
|
109
|
+
);
|
|
110
|
+
if (devices.length === 0) {
|
|
111
|
+
throw new Error("No ADB devices connected");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Get Quest IP
|
|
115
|
+
const questIp = await this.getQuestIp();
|
|
116
|
+
this.questIp = questIp;
|
|
117
|
+
|
|
118
|
+
// Ensure casting service APK is installed on Quest
|
|
119
|
+
// Use the ADB device serial (USB or configured), not the WiFi IP
|
|
120
|
+
const adbDevice = getAdbDevice() ?? await this.getAdbSerial() ?? `${questIp}:5555`;
|
|
121
|
+
await ensureCastingInstalled(adbDevice);
|
|
122
|
+
|
|
123
|
+
const listenPort = opts.listenPort ?? 4445;
|
|
124
|
+
const { width, height } = resolveResolution(opts.resolution, opts.width, opts.height);
|
|
125
|
+
|
|
126
|
+
// Create and start session
|
|
127
|
+
const session = new CastSession({ listenPort, width, height });
|
|
128
|
+
this.session = session;
|
|
129
|
+
|
|
130
|
+
// Wire session events → SSE broadcast
|
|
131
|
+
session.on("connected", () => this.broadcastStatus());
|
|
132
|
+
session.on("disconnected", () => this.broadcastStatus());
|
|
133
|
+
session.on("pose-loop", () => this.broadcastStatus());
|
|
134
|
+
|
|
135
|
+
// Complete all async setup before starting periodic work
|
|
136
|
+
await session.bind();
|
|
137
|
+
if (session.listenPort !== listenPort) {
|
|
138
|
+
verbose(
|
|
139
|
+
`Port ${listenPort} in use, listening on ${session.listenPort}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
await session.adbSetup(questIp);
|
|
143
|
+
await session.start(questIp);
|
|
144
|
+
|
|
145
|
+
// Stats interval created AFTER all async ops succeed
|
|
146
|
+
this.statsInterval = setInterval(() => {
|
|
147
|
+
if (session.connected) {
|
|
148
|
+
this.broadcastStatus();
|
|
149
|
+
}
|
|
150
|
+
}, 500);
|
|
151
|
+
|
|
152
|
+
verbose("Quest connected, casting active");
|
|
153
|
+
} finally {
|
|
154
|
+
this.starting = false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async stop(): Promise<void> {
|
|
159
|
+
await this.stopSession();
|
|
160
|
+
this.broadcastStatus();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async restart(): Promise<void> {
|
|
164
|
+
if (!this.session) {
|
|
165
|
+
await this.start();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.broadcastToast("Restarting cast\u2026");
|
|
169
|
+
await this.session.restart();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async stopSession(): Promise<void> {
|
|
173
|
+
if (this.statsInterval) {
|
|
174
|
+
clearInterval(this.statsInterval);
|
|
175
|
+
this.statsInterval = null;
|
|
176
|
+
}
|
|
177
|
+
if (this.session) {
|
|
178
|
+
try {
|
|
179
|
+
await this.session.stop();
|
|
180
|
+
} catch {
|
|
181
|
+
// Best effort
|
|
182
|
+
}
|
|
183
|
+
this.session = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Get the first connected ADB device serial (e.g. USB serial or IP:port) */
|
|
188
|
+
private async getAdbSerial(): Promise<string | null> {
|
|
189
|
+
const output = await execCommand("adb", ["devices"]);
|
|
190
|
+
const lines = output.trim().split("\n").slice(1);
|
|
191
|
+
const first = lines.find((l) => l.includes("device"));
|
|
192
|
+
if (!first) return null;
|
|
193
|
+
return first.split("\t")[0].trim();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async getQuestIp(): Promise<string> {
|
|
197
|
+
// If a device IP was configured, use it directly (strip :port if present)
|
|
198
|
+
if (this.configuredDevice) {
|
|
199
|
+
const ip = this.configuredDevice.split(":")[0];
|
|
200
|
+
verbose(`Using configured device: ${ip}`);
|
|
201
|
+
return ip;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const devOutput = await execCommand("adb", ["devices"]);
|
|
205
|
+
const devLines = devOutput.trim().split("\n").slice(1);
|
|
206
|
+
const firstDevice = devLines.find((l) => l.includes("device"));
|
|
207
|
+
if (!firstDevice) {
|
|
208
|
+
throw new Error("No authorized ADB device found");
|
|
209
|
+
}
|
|
210
|
+
const deviceId = firstDevice.split("\t")[0].trim();
|
|
211
|
+
try {
|
|
212
|
+
const ip = await execCommand("adb", [
|
|
213
|
+
"-s",
|
|
214
|
+
deviceId,
|
|
215
|
+
"shell",
|
|
216
|
+
"ip addr show wlan0 | grep 'inet ' | tr -s ' ' | cut -f3 -d' ' | cut -f1 -d/",
|
|
217
|
+
]);
|
|
218
|
+
const trimmed = ip.trim();
|
|
219
|
+
if (trimmed && !trimmed.includes("error")) {
|
|
220
|
+
return trimmed;
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// Fallback
|
|
224
|
+
}
|
|
225
|
+
return deviceId.split(":")[0];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- SSE ---
|
|
229
|
+
|
|
230
|
+
broadcast(event: string, data: unknown): void {
|
|
231
|
+
const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
232
|
+
for (const res of this.sseClients) {
|
|
233
|
+
try {
|
|
234
|
+
res.write(msg);
|
|
235
|
+
} catch {
|
|
236
|
+
this.sseClients.delete(res);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Broadcast full status snapshot as SSE "status" event. */
|
|
242
|
+
broadcastStatus(): void {
|
|
243
|
+
this.broadcast("status", this.getStatus());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
broadcastToast(msg: string): void {
|
|
247
|
+
this.broadcast("toast", { message: msg });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
addSSEClient(res: ServerResponse): void {
|
|
251
|
+
this.sseClients.add(res);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
removeSSEClient(res: ServerResponse): void {
|
|
255
|
+
this.sseClients.delete(res);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- Cleanup ---
|
|
259
|
+
|
|
260
|
+
cleanup(): void {
|
|
261
|
+
if (this.statsInterval) {
|
|
262
|
+
clearInterval(this.statsInterval);
|
|
263
|
+
this.statsInterval = null;
|
|
264
|
+
}
|
|
265
|
+
if (this.session) {
|
|
266
|
+
try {
|
|
267
|
+
this.session.stop();
|
|
268
|
+
} catch {
|
|
269
|
+
// Best effort sync cleanup
|
|
270
|
+
}
|
|
271
|
+
this.session = null;
|
|
272
|
+
}
|
|
273
|
+
for (const res of this.sseClients) {
|
|
274
|
+
try {
|
|
275
|
+
res.end();
|
|
276
|
+
} catch {
|
|
277
|
+
// Ignore
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
this.sseClients.clear();
|
|
281
|
+
}
|
|
282
|
+
}
|