@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,80 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
detectSubMagic,
|
|
4
|
+
isMgikMagic,
|
|
5
|
+
packMgikSub,
|
|
6
|
+
parseMgikSub,
|
|
7
|
+
} from "../src/mgik.js";
|
|
8
|
+
import { MGIK_SUB_HEADER_SIZE } from "../src/constants.js";
|
|
9
|
+
|
|
10
|
+
describe("MGIK", () => {
|
|
11
|
+
describe("packMgikSub / parseMgikSub round-trip", () => {
|
|
12
|
+
it("round-trips with typical values", () => {
|
|
13
|
+
const subMagic = 0xdeadbeef;
|
|
14
|
+
const msgSeq = 5;
|
|
15
|
+
const packed = packMgikSub(subMagic, msgSeq);
|
|
16
|
+
expect(packed.length).toBe(MGIK_SUB_HEADER_SIZE);
|
|
17
|
+
|
|
18
|
+
const parsed = parseMgikSub(packed);
|
|
19
|
+
expect(parsed).not.toBeNull();
|
|
20
|
+
expect(parsed!.subMagic).toBe(subMagic);
|
|
21
|
+
expect(parsed!.msgSeq).toBe(msgSeq);
|
|
22
|
+
expect(parsed!.ackSeq).toBe(msgSeq); // ack_seq defaults to msg_seq
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("includes fixed tail (0, 1, 2)", () => {
|
|
26
|
+
const packed = packMgikSub(0x12345678, 0);
|
|
27
|
+
expect(packed.readUInt32BE(12)).toBe(0);
|
|
28
|
+
expect(packed.readUInt32BE(16)).toBe(1);
|
|
29
|
+
expect(packed.readUInt32BE(20)).toBe(2);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("parseMgikSub", () => {
|
|
34
|
+
it("returns null for too-short buffer", () => {
|
|
35
|
+
expect(parseMgikSub(Buffer.alloc(20))).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("detectSubMagic", () => {
|
|
40
|
+
it("detects sub_magic from a valid Quest initial message", () => {
|
|
41
|
+
const buf = Buffer.alloc(MGIK_SUB_HEADER_SIZE);
|
|
42
|
+
buf.writeUInt32BE(0xaabbccdd, 0); // sub_magic
|
|
43
|
+
buf.writeUInt32BE(0, 4); // msg_seq
|
|
44
|
+
buf.writeUInt32BE(0, 8); // ack_seq
|
|
45
|
+
buf.writeUInt32BE(0, 12);
|
|
46
|
+
buf.writeUInt32BE(1, 16);
|
|
47
|
+
buf.writeUInt32BE(2, 20);
|
|
48
|
+
expect(detectSubMagic(buf)).toBe(0xaabbccdd);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns null if tail signature does not match", () => {
|
|
52
|
+
const buf = Buffer.alloc(MGIK_SUB_HEADER_SIZE);
|
|
53
|
+
buf.writeUInt32BE(0xaabbccdd, 0);
|
|
54
|
+
buf.writeUInt32BE(0, 12);
|
|
55
|
+
buf.writeUInt32BE(1, 16);
|
|
56
|
+
buf.writeUInt32BE(3, 20); // wrong: should be 2
|
|
57
|
+
expect(detectSubMagic(buf)).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns null for wrong-sized payload", () => {
|
|
61
|
+
expect(detectSubMagic(Buffer.alloc(16))).toBeNull();
|
|
62
|
+
expect(detectSubMagic(Buffer.alloc(32))).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("isMgikMagic", () => {
|
|
67
|
+
it("detects MGIK magic bytes", () => {
|
|
68
|
+
const buf = Buffer.from("MGIK", "ascii");
|
|
69
|
+
expect(isMgikMagic(buf)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rejects non-MGIK bytes", () => {
|
|
73
|
+
expect(isMgikMagic(Buffer.from("XRSP"))).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns false for short buffer", () => {
|
|
77
|
+
expect(isMgikMagic(Buffer.from("MG"))).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildActivateLayer,
|
|
4
|
+
buildConfig,
|
|
5
|
+
buildDisconnect,
|
|
6
|
+
buildDisplayConfig,
|
|
7
|
+
buildInit,
|
|
8
|
+
buildInputForwardingState,
|
|
9
|
+
buildKeepalive,
|
|
10
|
+
buildMud,
|
|
11
|
+
buildPose,
|
|
12
|
+
buildSetProperty,
|
|
13
|
+
buildShortAck,
|
|
14
|
+
buildStartInputForwarding,
|
|
15
|
+
buildVirtualMouse,
|
|
16
|
+
packMudString,
|
|
17
|
+
parseLayerConfiguration,
|
|
18
|
+
parseMudString,
|
|
19
|
+
} from "../src/mud.js";
|
|
20
|
+
import {
|
|
21
|
+
CMD_ACTIVATE_LAYER,
|
|
22
|
+
CMD_CONFIG,
|
|
23
|
+
CMD_DISCONNECT,
|
|
24
|
+
CMD_DISPLAY_CONFIG,
|
|
25
|
+
CMD_INIT,
|
|
26
|
+
CMD_INPUT_FORWARDING_STATE,
|
|
27
|
+
CMD_KEEPALIVE,
|
|
28
|
+
CMD_POSE,
|
|
29
|
+
CMD_SET_PROPERTY,
|
|
30
|
+
CMD_SHORT_ACK_65,
|
|
31
|
+
CMD_START_INPUT_FORWARDING,
|
|
32
|
+
CMD_VIRTUAL_MOUSE,
|
|
33
|
+
MGIK_SUB_HEADER_SIZE,
|
|
34
|
+
} from "../src/constants.js";
|
|
35
|
+
import type { PoseState } from "../src/types.js";
|
|
36
|
+
|
|
37
|
+
const SUB_MAGIC = 0xdeadbeef;
|
|
38
|
+
|
|
39
|
+
/** Helper: read command ID from payload after sub-header. */
|
|
40
|
+
function readCmd(payload: Buffer): number {
|
|
41
|
+
return payload.readUInt32BE(MGIK_SUB_HEADER_SIZE);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Helper: verify sub-header in payload. */
|
|
45
|
+
function verifySubHeader(payload: Buffer, subMagic: number, seq: number) {
|
|
46
|
+
expect(payload.readUInt32BE(0)).toBe(subMagic);
|
|
47
|
+
expect(payload.readUInt32BE(4)).toBe(seq);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("MUD strings", () => {
|
|
51
|
+
describe("packMudString / parseMudString round-trip", () => {
|
|
52
|
+
it("round-trips a simple string", () => {
|
|
53
|
+
const packed = packMudString("hello");
|
|
54
|
+
const { value, nextOffset } = parseMudString(packed, 0);
|
|
55
|
+
expect(value).toBe("hello");
|
|
56
|
+
// "hello" = 5 bytes, padded to 8, plus 4 byte length prefix = 12
|
|
57
|
+
expect(nextOffset).toBe(12);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("round-trips an aligned string (4 bytes)", () => {
|
|
61
|
+
const packed = packMudString("test");
|
|
62
|
+
const { value, nextOffset } = parseMudString(packed, 0);
|
|
63
|
+
expect(value).toBe("test");
|
|
64
|
+
expect(nextOffset).toBe(8); // 4 len + 4 data, no padding needed
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("round-trips empty string", () => {
|
|
68
|
+
const packed = packMudString("");
|
|
69
|
+
const { value, nextOffset } = parseMudString(packed, 0);
|
|
70
|
+
expect(value).toBe("");
|
|
71
|
+
expect(nextOffset).toBe(4); // just the length prefix
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("handles UTF-8", () => {
|
|
75
|
+
const packed = packMudString("caf\u00e9");
|
|
76
|
+
const { value } = parseMudString(packed, 0);
|
|
77
|
+
expect(value).toBe("caf\u00e9");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("parseMudString edge cases", () => {
|
|
82
|
+
it("returns empty for offset at buffer end", () => {
|
|
83
|
+
const { value, nextOffset } = parseMudString(Buffer.alloc(4), 4);
|
|
84
|
+
expect(value).toBe("");
|
|
85
|
+
expect(nextOffset).toBe(4);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("MUD builders", () => {
|
|
91
|
+
describe("buildInit", () => {
|
|
92
|
+
it("builds correct init payload", () => {
|
|
93
|
+
const payload = buildInit(SUB_MAGIC, 0);
|
|
94
|
+
verifySubHeader(payload, SUB_MAGIC, 0);
|
|
95
|
+
expect(readCmd(payload)).toBe(CMD_INIT);
|
|
96
|
+
// 16 zero bytes after command
|
|
97
|
+
const trailing = payload.subarray(MGIK_SUB_HEADER_SIZE + 4);
|
|
98
|
+
expect(trailing.length).toBe(16);
|
|
99
|
+
expect(trailing.every((b) => b === 0)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("buildConfig", () => {
|
|
104
|
+
it("encodes width, height, uuid, and timestamp", () => {
|
|
105
|
+
const payload = buildConfig(SUB_MAGIC, 1, 2064, 1162, "test-uuid", "12345");
|
|
106
|
+
verifySubHeader(payload, SUB_MAGIC, 1);
|
|
107
|
+
expect(readCmd(payload)).toBe(CMD_CONFIG);
|
|
108
|
+
// Width at sub_header(24) + cmd(4) + offset 4
|
|
109
|
+
expect(payload.readUInt32BE(MGIK_SUB_HEADER_SIZE + 4)).toBe(2064);
|
|
110
|
+
expect(payload.readUInt32BE(MGIK_SUB_HEADER_SIZE + 8)).toBe(1162);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("buildKeepalive", () => {
|
|
115
|
+
it("encodes ack value and strings", () => {
|
|
116
|
+
const payload = buildKeepalive(SUB_MAGIC, 5, 0xc8, "uuid1", "ts1");
|
|
117
|
+
verifySubHeader(payload, SUB_MAGIC, 5);
|
|
118
|
+
expect(readCmd(payload)).toBe(CMD_KEEPALIVE);
|
|
119
|
+
expect(payload.readUInt32BE(MGIK_SUB_HEADER_SIZE + 4)).toBe(0xc8);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("buildShortAck", () => {
|
|
124
|
+
it("encodes command and zero", () => {
|
|
125
|
+
const payload = buildShortAck(SUB_MAGIC, 3, CMD_SHORT_ACK_65);
|
|
126
|
+
verifySubHeader(payload, SUB_MAGIC, 3);
|
|
127
|
+
expect(readCmd(payload)).toBe(CMD_SHORT_ACK_65);
|
|
128
|
+
expect(payload.readUInt32BE(MGIK_SUB_HEADER_SIZE + 4)).toBe(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("buildPose", () => {
|
|
133
|
+
it("encodes pose with correct wire order", () => {
|
|
134
|
+
const pose: PoseState = {
|
|
135
|
+
x: 1.0, y: 2.0, z: 3.0,
|
|
136
|
+
qw: 1.0, qx: 0.0, qy: 0.0, qz: 0.0,
|
|
137
|
+
yaw: 0, pitch: 0,
|
|
138
|
+
};
|
|
139
|
+
const payload = buildPose(SUB_MAGIC, 0, pose);
|
|
140
|
+
verifySubHeader(payload, SUB_MAGIC, 0);
|
|
141
|
+
expect(readCmd(payload)).toBe(CMD_POSE);
|
|
142
|
+
const off = MGIK_SUB_HEADER_SIZE + 4;
|
|
143
|
+
// Wire order: type(0), posX, posY, posZ, qw, qx, qy, qz
|
|
144
|
+
expect(payload.readFloatBE(off)).toBeCloseTo(0.0); // type
|
|
145
|
+
expect(payload.readFloatBE(off + 4)).toBeCloseTo(1.0); // x
|
|
146
|
+
expect(payload.readFloatBE(off + 8)).toBeCloseTo(2.0); // y
|
|
147
|
+
expect(payload.readFloatBE(off + 12)).toBeCloseTo(3.0); // z
|
|
148
|
+
expect(payload.readFloatBE(off + 16)).toBeCloseTo(1.0); // qw
|
|
149
|
+
expect(payload.readFloatBE(off + 20)).toBeCloseTo(0.0); // qx
|
|
150
|
+
expect(payload.readFloatBE(off + 24)).toBeCloseTo(0.0); // qy
|
|
151
|
+
expect(payload.readFloatBE(off + 28)).toBeCloseTo(0.0); // qz
|
|
152
|
+
// Total: sub_header(24) + cmd(4) + 8 floats(32) = 60 bytes
|
|
153
|
+
expect(payload.length).toBe(MGIK_SUB_HEADER_SIZE + 36);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("buildDisplayConfig", () => {
|
|
158
|
+
it("encodes width and height", () => {
|
|
159
|
+
const payload = buildDisplayConfig(SUB_MAGIC, 0, 1920, 1080);
|
|
160
|
+
verifySubHeader(payload, SUB_MAGIC, 0);
|
|
161
|
+
expect(readCmd(payload)).toBe(CMD_DISPLAY_CONFIG);
|
|
162
|
+
expect(payload.readUInt32BE(MGIK_SUB_HEADER_SIZE + 4)).toBe(1920);
|
|
163
|
+
expect(payload.readUInt32BE(MGIK_SUB_HEADER_SIZE + 8)).toBe(1080);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("buildDisconnect", () => {
|
|
168
|
+
it("has only command ID", () => {
|
|
169
|
+
const payload = buildDisconnect(SUB_MAGIC, 0);
|
|
170
|
+
verifySubHeader(payload, SUB_MAGIC, 0);
|
|
171
|
+
expect(readCmd(payload)).toBe(CMD_DISCONNECT);
|
|
172
|
+
expect(payload.length).toBe(MGIK_SUB_HEADER_SIZE + 4);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("buildSetProperty", () => {
|
|
177
|
+
it("encodes name and value strings", () => {
|
|
178
|
+
const payload = buildSetProperty(SUB_MAGIC, 0, "key", "val");
|
|
179
|
+
verifySubHeader(payload, SUB_MAGIC, 0);
|
|
180
|
+
expect(readCmd(payload)).toBe(CMD_SET_PROPERTY);
|
|
181
|
+
// Parse name string after cmd
|
|
182
|
+
const nameResult = parseMudString(payload, MGIK_SUB_HEADER_SIZE + 4);
|
|
183
|
+
expect(nameResult.value).toBe("key");
|
|
184
|
+
const valueResult = parseMudString(payload, nameResult.nextOffset);
|
|
185
|
+
expect(valueResult.value).toBe("val");
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("buildVirtualMouse", () => {
|
|
190
|
+
it("encodes layer, action, and coordinates", () => {
|
|
191
|
+
const payload = buildVirtualMouse(SUB_MAGIC, 0, 5, 3, 0.5, 0.5);
|
|
192
|
+
verifySubHeader(payload, SUB_MAGIC, 0);
|
|
193
|
+
expect(readCmd(payload)).toBe(CMD_VIRTUAL_MOUSE);
|
|
194
|
+
const off = MGIK_SUB_HEADER_SIZE + 4;
|
|
195
|
+
expect(payload.readUInt32BE(off)).toBe(5); // layerId
|
|
196
|
+
expect(payload.readUInt32BE(off + 4)).toBe(3); // action
|
|
197
|
+
expect(payload.readFloatBE(off + 8)).toBeCloseTo(0.5); // x
|
|
198
|
+
expect(payload.readFloatBE(off + 12)).toBeCloseTo(0.5); // y
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("buildInputForwardingState", () => {
|
|
203
|
+
it("encodes state value", () => {
|
|
204
|
+
const payload = buildInputForwardingState(SUB_MAGIC, 0, 1);
|
|
205
|
+
expect(readCmd(payload)).toBe(CMD_INPUT_FORWARDING_STATE);
|
|
206
|
+
expect(payload.readUInt32BE(MGIK_SUB_HEADER_SIZE + 4)).toBe(1);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("buildStartInputForwarding", () => {
|
|
211
|
+
it("has only command ID", () => {
|
|
212
|
+
const payload = buildStartInputForwarding(SUB_MAGIC, 0);
|
|
213
|
+
expect(readCmd(payload)).toBe(CMD_START_INPUT_FORWARDING);
|
|
214
|
+
expect(payload.length).toBe(MGIK_SUB_HEADER_SIZE + 4);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("buildActivateLayer", () => {
|
|
219
|
+
it("encodes layer ID", () => {
|
|
220
|
+
const payload = buildActivateLayer(SUB_MAGIC, 0, 42);
|
|
221
|
+
expect(readCmd(payload)).toBe(CMD_ACTIVATE_LAYER);
|
|
222
|
+
expect(payload.readUInt32BE(MGIK_SUB_HEADER_SIZE + 4)).toBe(42);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("buildMud", () => {
|
|
227
|
+
it("builds arbitrary MUD with payload", () => {
|
|
228
|
+
const extra = Buffer.from([0x01, 0x02]);
|
|
229
|
+
const payload = buildMud(SUB_MAGIC, 0, 999, extra);
|
|
230
|
+
expect(readCmd(payload)).toBe(999);
|
|
231
|
+
expect(payload[MGIK_SUB_HEADER_SIZE + 4]).toBe(0x01);
|
|
232
|
+
expect(payload[MGIK_SUB_HEADER_SIZE + 5]).toBe(0x02);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("builds MUD without payload", () => {
|
|
236
|
+
const payload = buildMud(SUB_MAGIC, 0, 999);
|
|
237
|
+
expect(payload.length).toBe(MGIK_SUB_HEADER_SIZE + 4);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("parseLayerConfiguration", () => {
|
|
243
|
+
it("parses a full layer configuration", () => {
|
|
244
|
+
// Build a mock LayerConfiguration payload
|
|
245
|
+
const buf = Buffer.alloc(128);
|
|
246
|
+
let off = 0;
|
|
247
|
+
buf.writeUInt32BE(0x12c, off); off += 4; // cmd
|
|
248
|
+
buf.writeUInt32BE(7, off); off += 4; // id
|
|
249
|
+
buf.writeUInt32BE(1920, off); off += 4; // width
|
|
250
|
+
buf.writeUInt32BE(1080, off); off += 4; // height
|
|
251
|
+
buf.writeFloatBE(0.0, off); off += 4; // posX
|
|
252
|
+
buf.writeFloatBE(0.0, off); off += 4; // posY
|
|
253
|
+
buf.writeFloatBE(1.0, off); off += 4; // depth
|
|
254
|
+
buf.writeUInt32BE(0, off); off += 4; // type = PANEL_APP
|
|
255
|
+
|
|
256
|
+
// Strings: appName, layerName, serial, pkg
|
|
257
|
+
const strings = ["MyApp", "main", "SERIAL123", "com.test.app"];
|
|
258
|
+
for (const s of strings) {
|
|
259
|
+
const raw = Buffer.from(s);
|
|
260
|
+
buf.writeUInt32BE(raw.length, off); off += 4;
|
|
261
|
+
raw.copy(buf, off); off += raw.length;
|
|
262
|
+
off += (4 - (raw.length % 4)) % 4;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const result = parseLayerConfiguration(buf.subarray(0, off));
|
|
266
|
+
expect(result).not.toBeNull();
|
|
267
|
+
expect(result!.id).toBe(7);
|
|
268
|
+
expect(result!.width).toBe(1920);
|
|
269
|
+
expect(result!.height).toBe(1080);
|
|
270
|
+
expect(result!.depth).toBeCloseTo(1.0);
|
|
271
|
+
expect(result!.type).toBe(0);
|
|
272
|
+
expect(result!.typeName).toBe("PANEL_APP");
|
|
273
|
+
expect(result!.app).toBe("MyApp");
|
|
274
|
+
expect(result!.layer).toBe("main");
|
|
275
|
+
expect(result!.serial).toBe("SERIAL123");
|
|
276
|
+
expect(result!.pkg).toBe("com.test.app");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("returns null for too-short payload", () => {
|
|
280
|
+
expect(parseLayerConfiguration(Buffer.alloc(16))).toBeNull();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("handles unknown layer type", () => {
|
|
284
|
+
const buf = Buffer.alloc(32);
|
|
285
|
+
buf.writeUInt32BE(0x12c, 0);
|
|
286
|
+
buf.writeUInt32BE(1, 4);
|
|
287
|
+
buf.writeUInt32BE(800, 8);
|
|
288
|
+
buf.writeUInt32BE(600, 12);
|
|
289
|
+
buf.writeUInt32BE(99, 28); // unknown type
|
|
290
|
+
|
|
291
|
+
const result = parseLayerConfiguration(buf);
|
|
292
|
+
expect(result).not.toBeNull();
|
|
293
|
+
expect(result!.typeName).toBe("UNKNOWN(99)");
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createPoseState,
|
|
4
|
+
eulerToQuat,
|
|
5
|
+
screenToYawPitch,
|
|
6
|
+
setPoseOffset,
|
|
7
|
+
updatePose,
|
|
8
|
+
} from "../src/pose.js";
|
|
9
|
+
|
|
10
|
+
describe("pose", () => {
|
|
11
|
+
describe("createPoseState", () => {
|
|
12
|
+
it("returns identity pose at origin", () => {
|
|
13
|
+
const pose = createPoseState();
|
|
14
|
+
expect(pose.x).toBe(0);
|
|
15
|
+
expect(pose.y).toBe(0);
|
|
16
|
+
expect(pose.z).toBe(0);
|
|
17
|
+
expect(pose.qw).toBe(1);
|
|
18
|
+
expect(pose.qx).toBe(0);
|
|
19
|
+
expect(pose.qy).toBe(0);
|
|
20
|
+
expect(pose.qz).toBe(0);
|
|
21
|
+
expect(pose.yaw).toBe(0);
|
|
22
|
+
expect(pose.pitch).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("eulerToQuat", () => {
|
|
27
|
+
it("returns identity quaternion for zero yaw/pitch", () => {
|
|
28
|
+
const q = eulerToQuat(0, 0);
|
|
29
|
+
expect(q.qw).toBeCloseTo(1);
|
|
30
|
+
expect(q.qx).toBeCloseTo(0);
|
|
31
|
+
expect(q.qy).toBeCloseTo(0);
|
|
32
|
+
expect(q.qz).toBeCloseTo(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("produces unit quaternion for 90-degree yaw", () => {
|
|
36
|
+
const q = eulerToQuat(Math.PI / 2, 0);
|
|
37
|
+
const mag = Math.sqrt(q.qw ** 2 + q.qx ** 2 + q.qy ** 2 + q.qz ** 2);
|
|
38
|
+
expect(mag).toBeCloseTo(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("produces unit quaternion for arbitrary angles", () => {
|
|
42
|
+
const q = eulerToQuat(0.7, -0.3);
|
|
43
|
+
const mag = Math.sqrt(q.qw ** 2 + q.qx ** 2 + q.qy ** 2 + q.qz ** 2);
|
|
44
|
+
expect(mag).toBeCloseTo(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("handles 180-degree yaw", () => {
|
|
48
|
+
const q = eulerToQuat(Math.PI, 0);
|
|
49
|
+
const mag = Math.sqrt(q.qw ** 2 + q.qx ** 2 + q.qy ** 2 + q.qz ** 2);
|
|
50
|
+
expect(mag).toBeCloseTo(1);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("updatePose", () => {
|
|
55
|
+
it("applies yaw increment", () => {
|
|
56
|
+
const pose = createPoseState();
|
|
57
|
+
const updated = updatePose(pose, { dYaw: 0.5 });
|
|
58
|
+
expect(updated.yaw).toBeCloseTo(0.5);
|
|
59
|
+
// Position shouldn't change without forward/strafe
|
|
60
|
+
expect(updated.x).toBeCloseTo(0);
|
|
61
|
+
expect(updated.y).toBeCloseTo(0);
|
|
62
|
+
expect(updated.z).toBeCloseTo(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("clamps pitch to ±π/2", () => {
|
|
66
|
+
const pose = createPoseState();
|
|
67
|
+
const up = updatePose(pose, { dPitch: 5.0 });
|
|
68
|
+
expect(up.pitch).toBeCloseTo(Math.PI / 2);
|
|
69
|
+
|
|
70
|
+
const down = updatePose(pose, { dPitch: -5.0 });
|
|
71
|
+
expect(down.pitch).toBeCloseTo(-Math.PI / 2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("moves forward in facing direction", () => {
|
|
75
|
+
const pose = createPoseState(); // facing -Z (yaw=0)
|
|
76
|
+
const moved = updatePose(pose, { dForward: 1.0 });
|
|
77
|
+
expect(moved.z).toBeCloseTo(-1.0);
|
|
78
|
+
expect(moved.x).toBeCloseTo(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("moves forward in rotated direction", () => {
|
|
82
|
+
let pose = createPoseState();
|
|
83
|
+
pose = updatePose(pose, { dYaw: Math.PI / 2 }); // face -X
|
|
84
|
+
const moved = updatePose(pose, { dForward: 1.0 });
|
|
85
|
+
expect(moved.x).toBeCloseTo(-1.0);
|
|
86
|
+
expect(moved.z).toBeCloseTo(0, 5);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("strafes perpendicular to facing", () => {
|
|
90
|
+
const pose = createPoseState(); // facing -Z, strafe right = +X
|
|
91
|
+
const moved = updatePose(pose, { dStrafe: 1.0 });
|
|
92
|
+
expect(moved.x).toBeCloseTo(1.0);
|
|
93
|
+
expect(moved.z).toBeCloseTo(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("applies vertical movement", () => {
|
|
97
|
+
const pose = createPoseState();
|
|
98
|
+
const moved = updatePose(pose, { dUp: 0.5 });
|
|
99
|
+
expect(moved.y).toBeCloseTo(0.5);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("does not mutate the original state", () => {
|
|
103
|
+
const pose = createPoseState();
|
|
104
|
+
updatePose(pose, { dYaw: 1.0, dForward: 1.0 });
|
|
105
|
+
expect(pose.yaw).toBe(0);
|
|
106
|
+
expect(pose.x).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("updates quaternion after movement", () => {
|
|
110
|
+
const pose = createPoseState();
|
|
111
|
+
const moved = updatePose(pose, { dYaw: Math.PI / 4 });
|
|
112
|
+
// Quaternion should not be identity anymore
|
|
113
|
+
expect(moved.qw).not.toBeCloseTo(1);
|
|
114
|
+
// But should still be unit
|
|
115
|
+
const mag = Math.sqrt(moved.qw ** 2 + moved.qx ** 2 + moved.qy ** 2 + moved.qz ** 2);
|
|
116
|
+
expect(mag).toBeCloseTo(1);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("setPoseOffset", () => {
|
|
121
|
+
it("sets position fields", () => {
|
|
122
|
+
const pose = createPoseState();
|
|
123
|
+
const updated = setPoseOffset(pose, { x: 1, y: 2, z: 3 });
|
|
124
|
+
expect(updated.x).toBe(1);
|
|
125
|
+
expect(updated.y).toBe(2);
|
|
126
|
+
expect(updated.z).toBe(3);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("sets orientation fields", () => {
|
|
130
|
+
const pose = createPoseState();
|
|
131
|
+
const updated = setPoseOffset(pose, { yaw: 0.5, pitch: 0.3 });
|
|
132
|
+
expect(updated.yaw).toBeCloseTo(0.5);
|
|
133
|
+
expect(updated.pitch).toBeCloseTo(0.3);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("preserves unset fields", () => {
|
|
137
|
+
const pose = setPoseOffset(createPoseState(), { x: 5, yaw: 1 });
|
|
138
|
+
const updated = setPoseOffset(pose, { y: 10 });
|
|
139
|
+
expect(updated.x).toBe(5);
|
|
140
|
+
expect(updated.y).toBe(10);
|
|
141
|
+
expect(updated.yaw).toBeCloseTo(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("does not mutate the original", () => {
|
|
145
|
+
const pose = createPoseState();
|
|
146
|
+
setPoseOffset(pose, { x: 99 });
|
|
147
|
+
expect(pose.x).toBe(0);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("screenToYawPitch", () => {
|
|
152
|
+
it("maps screen center to current orientation", () => {
|
|
153
|
+
const result = screenToYawPitch(0.5, 0.5, 0, 0, 1920, 1080);
|
|
154
|
+
expect(result.yaw).toBeCloseTo(0);
|
|
155
|
+
expect(result.pitch).toBeCloseTo(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("maps left edge to positive yaw offset", () => {
|
|
159
|
+
const result = screenToYawPitch(0, 0.5, 0, 0, 1920, 1080);
|
|
160
|
+
expect(result.yaw).toBeGreaterThan(0);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("maps right edge to negative yaw offset", () => {
|
|
164
|
+
const result = screenToYawPitch(1, 0.5, 0, 0, 1920, 1080);
|
|
165
|
+
expect(result.yaw).toBeLessThan(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("respects current yaw", () => {
|
|
169
|
+
const result = screenToYawPitch(0.5, 0.5, 1.0, 0, 1920, 1080);
|
|
170
|
+
expect(result.yaw).toBeCloseTo(1.0);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
packXrsp,
|
|
4
|
+
parseXrspHeader,
|
|
5
|
+
xrspPayloadSize,
|
|
6
|
+
} from "../src/xrsp.js";
|
|
7
|
+
import {
|
|
8
|
+
XRSP_FLAGS_STANDARD,
|
|
9
|
+
XRSP_HEADER_SIZE,
|
|
10
|
+
XRSP_TOPIC_CAST,
|
|
11
|
+
} from "../src/constants.js";
|
|
12
|
+
|
|
13
|
+
describe("XRSP", () => {
|
|
14
|
+
describe("packXrsp / parseXrspHeader round-trip", () => {
|
|
15
|
+
it("round-trips with empty payload", () => {
|
|
16
|
+
const packed = packXrsp(0, Buffer.alloc(0));
|
|
17
|
+
expect(packed.length).toBe(XRSP_HEADER_SIZE); // header only, word_count=1
|
|
18
|
+
const header = parseXrspHeader(packed);
|
|
19
|
+
expect(header.flags).toBe(XRSP_FLAGS_STANDARD);
|
|
20
|
+
expect(header.topic).toBe(XRSP_TOPIC_CAST);
|
|
21
|
+
expect(header.wordCount).toBe(1);
|
|
22
|
+
expect(header.seq).toBe(0);
|
|
23
|
+
expect(xrspPayloadSize(header)).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("round-trips with 4-byte aligned payload", () => {
|
|
27
|
+
const payload = Buffer.from([0x01, 0x02, 0x03, 0x04]);
|
|
28
|
+
const packed = packXrsp(42, payload);
|
|
29
|
+
expect(packed.length).toBe(XRSP_HEADER_SIZE + 4);
|
|
30
|
+
const header = parseXrspHeader(packed);
|
|
31
|
+
expect(header.wordCount).toBe(2); // 4 bytes / 4 + 1
|
|
32
|
+
expect(header.seq).toBe(42);
|
|
33
|
+
expect(xrspPayloadSize(header)).toBe(4);
|
|
34
|
+
// Verify payload preserved
|
|
35
|
+
expect(packed.subarray(XRSP_HEADER_SIZE)).toEqual(payload);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("pads non-aligned payload to 4 bytes", () => {
|
|
39
|
+
const payload = Buffer.from([0xaa, 0xbb]); // 2 bytes → padded to 4
|
|
40
|
+
const packed = packXrsp(1, payload);
|
|
41
|
+
expect(packed.length).toBe(XRSP_HEADER_SIZE + 4);
|
|
42
|
+
const header = parseXrspHeader(packed);
|
|
43
|
+
expect(header.wordCount).toBe(2);
|
|
44
|
+
// First 2 bytes are payload, next 2 are zero padding
|
|
45
|
+
expect(packed[XRSP_HEADER_SIZE]).toBe(0xaa);
|
|
46
|
+
expect(packed[XRSP_HEADER_SIZE + 1]).toBe(0xbb);
|
|
47
|
+
expect(packed[XRSP_HEADER_SIZE + 2]).toBe(0);
|
|
48
|
+
expect(packed[XRSP_HEADER_SIZE + 3]).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("pads 5-byte payload to 8 bytes", () => {
|
|
52
|
+
const payload = Buffer.alloc(5, 0xff);
|
|
53
|
+
const packed = packXrsp(0, payload);
|
|
54
|
+
expect(packed.length).toBe(XRSP_HEADER_SIZE + 8);
|
|
55
|
+
const header = parseXrspHeader(packed);
|
|
56
|
+
expect(header.wordCount).toBe(3); // 8/4 + 1
|
|
57
|
+
expect(xrspPayloadSize(header)).toBe(8);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("preserves custom flags", () => {
|
|
61
|
+
const packed = packXrsp(0, Buffer.alloc(0), 0x18);
|
|
62
|
+
const header = parseXrspHeader(packed);
|
|
63
|
+
expect(header.flags).toBe(0x18);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("masks topic to 6 bits", () => {
|
|
67
|
+
// Topic byte on wire might have high bits, parseXrspHeader masks to 0x3F
|
|
68
|
+
const buf = Buffer.alloc(XRSP_HEADER_SIZE);
|
|
69
|
+
buf.writeUInt8(0x10, 0); // flags
|
|
70
|
+
buf.writeUInt8(0xc2, 1); // topic = 0xC2, masked = 0x02
|
|
71
|
+
buf.writeUInt16LE(1, 2);
|
|
72
|
+
const header = parseXrspHeader(buf);
|
|
73
|
+
expect(header.topic).toBe(2);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("parseXrspHeader", () => {
|
|
78
|
+
it("throws on too-short buffer", () => {
|
|
79
|
+
expect(() => parseXrspHeader(Buffer.alloc(4))).toThrow();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("sequence number wrapping", () => {
|
|
84
|
+
it("handles high sequence numbers", () => {
|
|
85
|
+
const packed = packXrsp(65535, Buffer.alloc(0));
|
|
86
|
+
const header = parseXrspHeader(packed);
|
|
87
|
+
expect(header.seq).toBe(65535);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./build",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"types": ["node"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "build"]
|
|
20
|
+
}
|