@stim-cli/server 1.11.0 → 1.13.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 +100 -14
- package/dist/{protocol-CmlG7X7_.mjs → protocol-rC-5KT1O.mjs} +175 -7
- package/dist/protocol.d.mts +120 -8
- package/dist/protocol.mjs +2 -2
- package/dist/protocol.schema.json +408 -3
- package/dist/stim-frames/EmulatorPosture.swift +73 -0
- package/dist/stim-frames/EmulatorRotation.swift +59 -0
- package/dist/stim-frames/InputMessages.swift +56 -6
- package/dist/stim-frames/ScreenshotMessages.swift +27 -4
- package/dist/stim-frames/SimulatorHID.swift +256 -21
- package/dist/stim-frames/SimulatorRotation.swift +64 -0
- package/dist/stim-frames/main.swift +299 -36
- package/dist/stim-frames/sim-fold.entitlements +8 -0
- package/dist/stim-frames/sim-fold.m +47 -0
- package/dist/stim-server.mjs +1080 -181
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -107,12 +107,13 @@ closes its open connections.
|
|
|
107
107
|
## Scopes
|
|
108
108
|
|
|
109
109
|
A paired device has the `read` capability, which serves state, or also
|
|
110
|
-
`control`, which runs [actions](#actions). Pairing grants `read` only, unless
|
|
110
|
+
`control`, which runs [actions](#actions) and [controls devices](#control). Pairing grants `read` only, unless
|
|
111
111
|
the pairing code came from `stim-server pair --control`. On the Mac,
|
|
112
112
|
`stim-server devices grant <id> --control` adds control to a paired device and
|
|
113
113
|
`--read` takes it away. Nothing a client sends changes its own capabilities.
|
|
114
|
-
The server checks the device's capabilities in `devices.json` on every action
|
|
115
|
-
|
|
114
|
+
The server checks the device's capabilities in `devices.json` on every action
|
|
115
|
+
and control session, and ends a device's control sessions when it loses
|
|
116
|
+
`control`, so taking control away applies to open connections at once. `hello` reports
|
|
116
117
|
the capabilities and actions of the connection's device when it connects; a
|
|
117
118
|
connection sees a new grant after it reconnects.
|
|
118
119
|
|
|
@@ -164,23 +165,31 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
164
165
|
`$STIM_HOME/server/helpers/`, named by a hash of the sources and the
|
|
165
166
|
compiler version. For a simulator it renders the display's framebuffer
|
|
166
167
|
(CoreSimulator's IOSurface) when the display reports damage, turned
|
|
167
|
-
upright
|
|
168
|
+
upright. An iPhone Duo lights one of its two panels, the cover while
|
|
169
|
+
folded and the inner panel while unfolded, and leaves the other black, so
|
|
170
|
+
the helper streams whichever panel is lit; its frames and video carry
|
|
171
|
+
`posture`, and the size changes with the panel. For an emulator it keeps one gRPC `streamScreenshot` call open,
|
|
168
172
|
found through the discovery file and token the emulator writes when Stim
|
|
169
173
|
boots it. It scales frames to fit the largest `maxEdge` and paces them to
|
|
170
174
|
the highest `fps` its subscribers asked for. All subscribers of a device
|
|
171
|
-
share one helper, which sends a new subscriber the latest frame
|
|
172
|
-
|
|
175
|
+
share one helper, which sends a new subscriber the latest frame. It keeps
|
|
176
|
+
running for 10 seconds after the last subscriber leaves, so a client that
|
|
177
|
+
subscribes for one frame at a time reuses it and gets the latest frame at
|
|
178
|
+
once, and exits after that or when the server's end of its stdin closes.
|
|
173
179
|
|
|
174
180
|
A client that decodes H.264 adds `video: ["h264"]`. When the helper is
|
|
175
181
|
built, the result carries `video: "h264"`, `fps` may go up to 60, and
|
|
176
182
|
frames arrive as binary WebSocket messages instead of `frame` events: a
|
|
177
183
|
big-endian header (u8 version 1, u8 flags with bit 0 set on a keyframe,
|
|
184
|
+
and on an iPhone Duo bit 1 while folded or bit 2 while unfolded,
|
|
178
185
|
u16 header length, u32 sequence number of the messages sent on this
|
|
179
186
|
subscription, f64 capture time in milliseconds since the epoch on the
|
|
180
187
|
Mac's clock, u16 width, u16 height, u8 subscription id length and the
|
|
181
188
|
ASCII id), then one Annex-B access unit. Every keyframe carries its SPS
|
|
182
189
|
and PPS, and the stream has no B-frames, so each access unit is shown as
|
|
183
|
-
it arrives.
|
|
190
|
+
it arrives. A change of size, such as a rotation or a Duo fold, restarts
|
|
191
|
+
the encoder, and the next access unit is a keyframe with the new SPS and
|
|
192
|
+
PPS. The helper encodes with VideoToolbox in real time: Main
|
|
184
193
|
profile, a keyframe at least every 2 seconds, straight from the
|
|
185
194
|
simulator's IOSurface or from the emulator's RGBA frames, only when the
|
|
186
195
|
screen changed. A subscriber starts at a keyframe, and `frames.keyframe`
|
|
@@ -194,13 +203,13 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
194
203
|
subscribers of the same device still get at most the `fps` they asked
|
|
195
204
|
for. Watching video needs only `read`. Without the helper, the result has
|
|
196
205
|
no `video`, `fps` above 30 is lowered to 30, and `frame` events arrive as
|
|
197
|
-
before;
|
|
198
|
-
|
|
206
|
+
before; a helper that fails before its first frame also falls back to
|
|
207
|
+
`frame` events within a video subscription.
|
|
199
208
|
|
|
200
209
|
Without the helper (the compiler is missing or fails, which the server
|
|
201
|
-
retries every 5 minutes, or the helper fails before its first frame),
|
|
202
|
-
subscription made while it is still being built,
|
|
203
|
-
|
|
210
|
+
retries every 5 minutes, or the helper fails before its first frame), and
|
|
211
|
+
for a subscription made while it is still being built, frames come from
|
|
212
|
+
screenshots, and `fps` and `maxEdge` only cap the rate. Simulators are
|
|
204
213
|
captured with `xcrun simctl io <udid> screenshot`, of the primary display,
|
|
205
214
|
or of the default display when that `simctl` does not accept `primary`. An
|
|
206
215
|
iPhone Duo lights one of two panels: the capture follows the lit one
|
|
@@ -221,12 +230,15 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
221
230
|
(`default` when absent), and returns the payload of
|
|
222
231
|
`stim <platform> --plan --json` run in the workspace: the fingerprint, the
|
|
223
232
|
cache result the next build would get (`local`, `remote` or `false`), the
|
|
224
|
-
prebuild decision,
|
|
233
|
+
prebuild decision, `expectedMs` with its `basis`, and on a predicted cold
|
|
234
|
+
build its `missReason`. It builds, boots and
|
|
225
235
|
installs nothing and writes no Stim state; a remote cache check can
|
|
226
236
|
download the artifact into a temporary directory, so it gets 150 seconds
|
|
227
237
|
instead of 60. A plan predicting that the build would refuse is a result
|
|
228
238
|
whose `refusal` holds the code, message and remedy. A plan that cannot be
|
|
229
|
-
computed is a `stim-failed` error.
|
|
239
|
+
computed is a `stim-failed` error. One plan runs per workspace at a time,
|
|
240
|
+
across all connections; later requests wait their turn, and the 150
|
|
241
|
+
seconds start when the plan starts.
|
|
230
242
|
- `machine.get` returns cheap machine usage, read in the server process
|
|
231
243
|
without running `stim`: `volumes`, one per volume that holds a Stim
|
|
232
244
|
workspace, Stim home, or the simulators, with `mount`, `holds`, `freeBytes`
|
|
@@ -297,6 +309,80 @@ and `durationMs` when it ran. Strings from the client and error messages are
|
|
|
297
309
|
cut to 256 characters. `stim-server log` prints them, with control characters
|
|
298
310
|
replaced by `?`.
|
|
299
311
|
|
|
312
|
+
## Control
|
|
313
|
+
|
|
314
|
+
A device with `control` can drive a simulator or emulator that `stim status`
|
|
315
|
+
lists as owned by a workspace. Nothing it sends reaches any other device.
|
|
316
|
+
|
|
317
|
+
- `control.begin` takes `workspace`, `platform`, `slot` (`default` when
|
|
318
|
+
absent) and `takeOver`, and returns `{ "session", "platform", "lease",
|
|
319
|
+
"postures" }`. `postures` lists what `input.posture` takes for the device:
|
|
320
|
+
`folded` and `unfolded` for an iPhone Duo, `folded`, `half-open` and
|
|
321
|
+
`unfolded` for an emulator with a hinge, such as a `pixel_fold` AVD, and
|
|
322
|
+
none otherwise.
|
|
323
|
+
The server refuses with `device-busy` when status reports the device driven
|
|
324
|
+
(agent-device, a `stim device lock`, a test runner) or another client
|
|
325
|
+
controls it, naming the driver. With `takeOver: true` it proceeds anyway;
|
|
326
|
+
another client's session then ends with `taken-over`.
|
|
327
|
+
- The session holds a `stim device lock <platform> <id> --for 2m` lease,
|
|
328
|
+
renewed every minute, so `stim status` shows the device as driven by
|
|
329
|
+
`stim device lock` and agents leave it alone. `lease` carries its
|
|
330
|
+
`grantedAt`, the `since` of that driver, so a client can tell its own lease
|
|
331
|
+
from an agent's. The server releases the lease with `stim device unlock`
|
|
332
|
+
when the session ends, unless the lease was already held before the session
|
|
333
|
+
began, as when it took the device over from an agent in the same workspace;
|
|
334
|
+
it renews only a lease it took. Leases belong to the workspace, not to the
|
|
335
|
+
session: an agent in the same workspace that runs `stim device lock` on the
|
|
336
|
+
device during a session shares the lease, and the session's end releases
|
|
337
|
+
it.
|
|
338
|
+
- `input.touch` takes `session`, `phase` (`down`, `move`, `up`), `x` and `y`
|
|
339
|
+
as fractions of the upright screen, and `display` (0, the main display).
|
|
340
|
+
`input.text` takes up to 256 printable ASCII characters, where `\n` presses
|
|
341
|
+
Return, `\t` Tab and `\b` Delete. `input.button` takes `home` or `lock`,
|
|
342
|
+
and on Android also `back` or `app-switch`. `input.rotate` takes
|
|
343
|
+
`direction` (`left` or `right`) and turns the device a quarter turn.
|
|
344
|
+
`input.posture` takes one of the session's `postures`. Each answers `{}` once the input
|
|
345
|
+
is handed to the device: when it goes through the helper, that is when the
|
|
346
|
+
helper receives it, so a failure there shows only in the server's log. A connection may send 120 inputs a second and type 40
|
|
347
|
+
characters a second, with a burst of 256, and rotate or change posture twice a
|
|
348
|
+
second; more fail with `limit-exceeded`.
|
|
349
|
+
- `control.end` ends a session. The server also ends it with a
|
|
350
|
+
`control-ended` event `{ "session", "reason", "message" }` after 5 minutes
|
|
351
|
+
without input (`idle`), when another client takes the device over
|
|
352
|
+
(`taken-over`), when the device stops or changes owner (`device-gone`),
|
|
353
|
+
when the paired device loses `control` (`forbidden`), or when input cannot
|
|
354
|
+
reach the device (`failed`). Closing the connection ends its sessions.
|
|
355
|
+
|
|
356
|
+
Input goes through the device's `stim-frames` helper, the process that
|
|
357
|
+
streams its frames:
|
|
358
|
+
|
|
359
|
+
- Simulators take touches, keys and buttons through the simulator's
|
|
360
|
+
CoreDevice HID service (`dtuhidd`), the one Xcode's Device Hub uses, so
|
|
361
|
+
input keeps working while Device Hub or Siniulator shows the simulator.
|
|
362
|
+
With an Xcode whose simulators have no such service, input goes through
|
|
363
|
+
SimulatorKit's legacy HID client instead. The first input starts that service, as Device
|
|
364
|
+
Hub does, and from then until the simulator reboots it ignores tools that
|
|
365
|
+
still use SimulatorKit's legacy HID client. Text is typed key by key on a US
|
|
366
|
+
layout. `lock` is the side button.
|
|
367
|
+
- Emulators take touches through the emulator's gRPC `sendTouch`. Text and
|
|
368
|
+
buttons go through gRPC `sendKey` when the emulator reports a hardware
|
|
369
|
+
keyboard, which AVDs Stim creates have. An emulator without one drops key
|
|
370
|
+
events, so for it text and buttons go through `adb -s <serial> shell input`.
|
|
371
|
+
- Rotation and posture go the way Stim Desktop sends them: a simulator turns
|
|
372
|
+
through the orientation message Simulator.app sends it, and an emulator
|
|
373
|
+
through gRPC `setPhysicalModel` for rotation and `setPosture` for its hinge.
|
|
374
|
+
An iPhone Duo folds with `sim-fold`, which the server builds from Stim
|
|
375
|
+
Desktop's sources on the first fold and runs inside the simulator with
|
|
376
|
+
`xcrun simctl spawn`; it answers `{}` once the fold finished, and fails
|
|
377
|
+
with `action-failed` when the fold does not finish within 40 seconds. `sim-fold`
|
|
378
|
+
swaps the posture, so the server runs it only when the Duo's latest frame
|
|
379
|
+
shows the other posture, and refuses the request until a frame showed one.
|
|
380
|
+
|
|
381
|
+
Every session start, takeover and end, and every refused `control.begin`,
|
|
382
|
+
appends a line to the action log, with `action` set to `control.begin`,
|
|
383
|
+
`control.take-over` or `control.end`, and a `reason` that says why the session
|
|
384
|
+
ended or whom it took the device from. Inputs are not logged.
|
|
385
|
+
|
|
300
386
|
The error codes `unauthorized`, `pairing-expired`, and `protocol-unsupported`
|
|
301
387
|
refuse the client until it pairs again or updates; clients retry the others.
|
|
302
388
|
|
|
@@ -16,7 +16,14 @@ const METHODS = [
|
|
|
16
16
|
"machine.get",
|
|
17
17
|
"machine.history",
|
|
18
18
|
"unsubscribe",
|
|
19
|
-
"action"
|
|
19
|
+
"action",
|
|
20
|
+
"control.begin",
|
|
21
|
+
"control.end",
|
|
22
|
+
"input.touch",
|
|
23
|
+
"input.text",
|
|
24
|
+
"input.button",
|
|
25
|
+
"input.rotate",
|
|
26
|
+
"input.posture"
|
|
20
27
|
];
|
|
21
28
|
/**
|
|
22
29
|
* `unauthorized`, `pairing-expired` and `protocol-unsupported` refuse the client until it pairs again or
|
|
@@ -41,7 +48,9 @@ const ERROR_CODES = [
|
|
|
41
48
|
"forbidden",
|
|
42
49
|
"unknown-action",
|
|
43
50
|
"action-busy",
|
|
44
|
-
"action-failed"
|
|
51
|
+
"action-failed",
|
|
52
|
+
"device-busy",
|
|
53
|
+
"unknown-session"
|
|
45
54
|
];
|
|
46
55
|
const LOG_SOURCES = [
|
|
47
56
|
"metro",
|
|
@@ -58,6 +67,12 @@ const LOG_LEVELS = [
|
|
|
58
67
|
"fatal"
|
|
59
68
|
];
|
|
60
69
|
const MAX_LOG_TAIL = 5e3;
|
|
70
|
+
/** `reload` also reaches the workspace's Stim-owned Chrome page. */
|
|
71
|
+
const RELOAD_PLATFORMS = [
|
|
72
|
+
"ios",
|
|
73
|
+
"android",
|
|
74
|
+
"web"
|
|
75
|
+
];
|
|
61
76
|
const FRAME_FPS = {
|
|
62
77
|
default: 5,
|
|
63
78
|
max: 30,
|
|
@@ -72,8 +87,38 @@ const VIDEO_CODECS = ["h264"];
|
|
|
72
87
|
const VIDEO_HEADER_VERSION = 1;
|
|
73
88
|
/** The keyframe bit of a {@link VideoPacket}'s flags. */
|
|
74
89
|
const VIDEO_KEYFRAME = 1;
|
|
90
|
+
/** The flag bits of a {@link VideoPacket} that carry an iPhone Duo's posture, as `posture` on a `frame` event. */
|
|
91
|
+
const VIDEO_FOLDED = 2;
|
|
92
|
+
const VIDEO_UNFOLDED = 4;
|
|
75
93
|
/** Each action runs one fixed `stim` command in the workspace. */
|
|
76
94
|
const ACTIONS = ["reload", "stop"];
|
|
95
|
+
const TOUCH_PHASES = [
|
|
96
|
+
"down",
|
|
97
|
+
"move",
|
|
98
|
+
"up"
|
|
99
|
+
];
|
|
100
|
+
const MAX_INPUT_TEXT = 256;
|
|
101
|
+
/** `home` and `lock` on both platforms; `back` and `app-switch` on Android only. */
|
|
102
|
+
const INPUT_BUTTONS = [
|
|
103
|
+
"home",
|
|
104
|
+
"lock",
|
|
105
|
+
"back",
|
|
106
|
+
"app-switch"
|
|
107
|
+
];
|
|
108
|
+
/** `left` turns the device a quarter turn counterclockwise, `right` clockwise. */
|
|
109
|
+
const ROTATE_DIRECTIONS = ["left", "right"];
|
|
110
|
+
const DEVICE_POSTURES = [
|
|
111
|
+
"folded",
|
|
112
|
+
"half-open",
|
|
113
|
+
"unfolded"
|
|
114
|
+
];
|
|
115
|
+
const CONTROL_END_REASONS = [
|
|
116
|
+
"idle",
|
|
117
|
+
"taken-over",
|
|
118
|
+
"device-gone",
|
|
119
|
+
"forbidden",
|
|
120
|
+
"failed"
|
|
121
|
+
];
|
|
77
122
|
const requestId = { type: ["integer", "string"] };
|
|
78
123
|
const protocolError = {
|
|
79
124
|
type: "object",
|
|
@@ -103,6 +148,17 @@ function request(method, params) {
|
|
|
103
148
|
}
|
|
104
149
|
};
|
|
105
150
|
}
|
|
151
|
+
function session(properties, required = []) {
|
|
152
|
+
return {
|
|
153
|
+
type: "object",
|
|
154
|
+
required: ["session", ...required],
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
properties: {
|
|
157
|
+
session: { type: "string" },
|
|
158
|
+
...properties
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
106
162
|
function optionalParams(method, params) {
|
|
107
163
|
return {
|
|
108
164
|
type: "object",
|
|
@@ -216,7 +272,12 @@ function protocolJsonSchema() {
|
|
|
216
272
|
ts: { type: "number" },
|
|
217
273
|
src: { type: "string" },
|
|
218
274
|
level: { type: "string" },
|
|
219
|
-
msg: { type: "string" }
|
|
275
|
+
msg: { type: "string" },
|
|
276
|
+
context: {
|
|
277
|
+
type: "array",
|
|
278
|
+
items: { type: "string" },
|
|
279
|
+
description: "With `errors`: the code frame and stack lines Expo printed after this error."
|
|
280
|
+
}
|
|
220
281
|
}
|
|
221
282
|
},
|
|
222
283
|
LogFilter: {
|
|
@@ -236,10 +297,11 @@ function protocolJsonSchema() {
|
|
|
236
297
|
level: { enum: [...LOG_LEVELS] },
|
|
237
298
|
slot: {
|
|
238
299
|
type: "string",
|
|
239
|
-
|
|
300
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$"
|
|
240
301
|
},
|
|
241
302
|
grep: {
|
|
242
303
|
type: "string",
|
|
304
|
+
pattern: "^[^\\u0000]*$",
|
|
243
305
|
description: "A regular expression matched against each message."
|
|
244
306
|
},
|
|
245
307
|
errors: { type: "boolean" },
|
|
@@ -295,7 +357,7 @@ function protocolJsonSchema() {
|
|
|
295
357
|
type: "string",
|
|
296
358
|
description: "An environment path from a status payload."
|
|
297
359
|
},
|
|
298
|
-
platform: { enum: [
|
|
360
|
+
platform: { enum: [...RELOAD_PLATFORMS] }
|
|
299
361
|
}
|
|
300
362
|
}, {
|
|
301
363
|
type: "object",
|
|
@@ -326,6 +388,61 @@ function protocolJsonSchema() {
|
|
|
326
388
|
}
|
|
327
389
|
}
|
|
328
390
|
},
|
|
391
|
+
ControlBeginParams: {
|
|
392
|
+
type: "object",
|
|
393
|
+
required: ["workspace", "platform"],
|
|
394
|
+
additionalProperties: false,
|
|
395
|
+
properties: {
|
|
396
|
+
workspace: {
|
|
397
|
+
type: "string",
|
|
398
|
+
description: "An environment path from a status payload."
|
|
399
|
+
},
|
|
400
|
+
platform: { enum: ["ios", "android"] },
|
|
401
|
+
slot: {
|
|
402
|
+
type: "string",
|
|
403
|
+
minLength: 1,
|
|
404
|
+
default: "default"
|
|
405
|
+
},
|
|
406
|
+
takeOver: {
|
|
407
|
+
type: "boolean",
|
|
408
|
+
default: false
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
ControlBeginResult: {
|
|
413
|
+
type: "object",
|
|
414
|
+
required: [
|
|
415
|
+
"session",
|
|
416
|
+
"platform",
|
|
417
|
+
"lease",
|
|
418
|
+
"postures"
|
|
419
|
+
],
|
|
420
|
+
additionalProperties: false,
|
|
421
|
+
properties: {
|
|
422
|
+
session: { type: "string" },
|
|
423
|
+
platform: { enum: ["ios", "android"] },
|
|
424
|
+
lease: { oneOf: [{ type: "null" }, {
|
|
425
|
+
type: "object",
|
|
426
|
+
required: ["grantedAt", "expiresAt"],
|
|
427
|
+
additionalProperties: false,
|
|
428
|
+
properties: {
|
|
429
|
+
grantedAt: {
|
|
430
|
+
type: ["string", "null"],
|
|
431
|
+
format: "date-time"
|
|
432
|
+
},
|
|
433
|
+
expiresAt: {
|
|
434
|
+
type: "string",
|
|
435
|
+
format: "date-time"
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}] },
|
|
439
|
+
postures: {
|
|
440
|
+
type: "array",
|
|
441
|
+
items: { enum: [...DEVICE_POSTURES] },
|
|
442
|
+
uniqueItems: true
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
},
|
|
329
446
|
BuildPlanParams: {
|
|
330
447
|
type: "object",
|
|
331
448
|
required: ["workspace", "platform"],
|
|
@@ -495,7 +612,41 @@ function protocolJsonSchema() {
|
|
|
495
612
|
additionalProperties: false,
|
|
496
613
|
properties: { subscription: { type: "string" } }
|
|
497
614
|
}),
|
|
498
|
-
request("action", { $ref: "#/$defs/ActionParams" })
|
|
615
|
+
request("action", { $ref: "#/$defs/ActionParams" }),
|
|
616
|
+
request("control.begin", { $ref: "#/$defs/ControlBeginParams" }),
|
|
617
|
+
request("control.end", session({})),
|
|
618
|
+
request("input.touch", session({
|
|
619
|
+
phase: { enum: [...TOUCH_PHASES] },
|
|
620
|
+
x: {
|
|
621
|
+
type: "number",
|
|
622
|
+
minimum: 0,
|
|
623
|
+
maximum: 1
|
|
624
|
+
},
|
|
625
|
+
y: {
|
|
626
|
+
type: "number",
|
|
627
|
+
minimum: 0,
|
|
628
|
+
maximum: 1
|
|
629
|
+
},
|
|
630
|
+
display: {
|
|
631
|
+
type: "integer",
|
|
632
|
+
minimum: 0,
|
|
633
|
+
maximum: 3,
|
|
634
|
+
default: 0
|
|
635
|
+
}
|
|
636
|
+
}, [
|
|
637
|
+
"phase",
|
|
638
|
+
"x",
|
|
639
|
+
"y"
|
|
640
|
+
])),
|
|
641
|
+
request("input.text", session({ text: {
|
|
642
|
+
type: "string",
|
|
643
|
+
minLength: 1,
|
|
644
|
+
maxLength: 256,
|
|
645
|
+
pattern: "^[\\x20-\\x7e\\n\\t\\b]+$"
|
|
646
|
+
} }, ["text"])),
|
|
647
|
+
request("input.button", session({ button: { enum: [...INPUT_BUTTONS] } }, ["button"])),
|
|
648
|
+
request("input.rotate", session({ direction: { enum: [...ROTATE_DIRECTIONS] } }, ["direction"])),
|
|
649
|
+
request("input.posture", session({ posture: { enum: [...DEVICE_POSTURES] } }, ["posture"]))
|
|
499
650
|
] },
|
|
500
651
|
ServerResponse: { oneOf: [{
|
|
501
652
|
type: "object",
|
|
@@ -505,6 +656,7 @@ function protocolJsonSchema() {
|
|
|
505
656
|
id: requestId,
|
|
506
657
|
result: { anyOf: [
|
|
507
658
|
{ $ref: "#/$defs/HelloResult" },
|
|
659
|
+
{ $ref: "#/$defs/ControlBeginResult" },
|
|
508
660
|
{ $ref: "#/$defs/ActionResult" },
|
|
509
661
|
{ $ref: "#/$defs/MachineUsage" },
|
|
510
662
|
{ $ref: "#/$defs/MachineHistory" },
|
|
@@ -627,6 +779,22 @@ function protocolJsonSchema() {
|
|
|
627
779
|
delayed: { type: "boolean" }
|
|
628
780
|
}
|
|
629
781
|
},
|
|
782
|
+
{
|
|
783
|
+
type: "object",
|
|
784
|
+
required: [
|
|
785
|
+
"event",
|
|
786
|
+
"session",
|
|
787
|
+
"reason",
|
|
788
|
+
"message"
|
|
789
|
+
],
|
|
790
|
+
additionalProperties: false,
|
|
791
|
+
properties: {
|
|
792
|
+
event: { const: "control-ended" },
|
|
793
|
+
session: { type: "string" },
|
|
794
|
+
reason: { enum: [...CONTROL_END_REASONS] },
|
|
795
|
+
message: { type: "string" }
|
|
796
|
+
}
|
|
797
|
+
},
|
|
630
798
|
{
|
|
631
799
|
type: "object",
|
|
632
800
|
required: [
|
|
@@ -651,4 +819,4 @@ function protocolJsonSchema() {
|
|
|
651
819
|
};
|
|
652
820
|
}
|
|
653
821
|
//#endregion
|
|
654
|
-
export {
|
|
822
|
+
export { VIDEO_UNFOLDED as C, VIDEO_KEYFRAME as S, ROTATE_DIRECTIONS as _, ERROR_CODES as a, VIDEO_FOLDED as b, INPUT_BUTTONS as c, MAX_INPUT_TEXT as d, MAX_LOG_TAIL as f, RELOAD_PLATFORMS as g, PROTOCOL_VERSION as h, DEVICE_POSTURES as i, LOG_LEVELS as l, PROTOCOL_SCHEMA_FILE as m, CAPABILITIES as n, FRAME_EDGE as o, METHODS as p, CONTROL_END_REASONS as r, FRAME_FPS as s, ACTIONS as t, LOG_SOURCES as u, TOUCH_PHASES as v, protocolJsonSchema as w, VIDEO_HEADER_VERSION as x, VIDEO_CODECS as y };
|
package/dist/protocol.d.mts
CHANGED
|
@@ -5,13 +5,13 @@ declare const PROTOCOL_SCHEMA_FILE = "protocol.schema.json";
|
|
|
5
5
|
/** `read` serves state. `control` also runs {@link ACTIONS}; only the Mac grants it. */
|
|
6
6
|
declare const CAPABILITIES: readonly ["read", "control"];
|
|
7
7
|
type Capability = (typeof CAPABILITIES)[number];
|
|
8
|
-
declare const METHODS: readonly ["hello", "status.subscribe", "logs.query", "logs.subscribe", "stats.get", "settings.get", "frames.subscribe", "frames.keyframe", "build.plan", "machine.get", "machine.history", "unsubscribe", "action"];
|
|
8
|
+
declare const METHODS: readonly ["hello", "status.subscribe", "logs.query", "logs.subscribe", "stats.get", "settings.get", "frames.subscribe", "frames.keyframe", "build.plan", "machine.get", "machine.history", "unsubscribe", "action", "control.begin", "control.end", "input.touch", "input.text", "input.button", "input.rotate", "input.posture"];
|
|
9
9
|
type Method = (typeof METHODS)[number];
|
|
10
10
|
/**
|
|
11
11
|
* `unauthorized`, `pairing-expired` and `protocol-unsupported` refuse the client until it pairs again or
|
|
12
12
|
* updates; clients retry the others.
|
|
13
13
|
*/
|
|
14
|
-
declare const ERROR_CODES: readonly ["unauthorized", "pairing-expired", "protocol-unsupported", "identity-unavailable", "bad-request", "unknown-method", "already-authenticated", "unknown-subscription", "unknown-workspace", "limit-exceeded", "slow-client", "status-failed", "logs-failed", "stim-failed", "frames-failed", "forbidden", "unknown-action", "action-busy", "action-failed"];
|
|
14
|
+
declare const ERROR_CODES: readonly ["unauthorized", "pairing-expired", "protocol-unsupported", "identity-unavailable", "bad-request", "unknown-method", "already-authenticated", "unknown-subscription", "unknown-workspace", "limit-exceeded", "slow-client", "status-failed", "logs-failed", "stim-failed", "frames-failed", "forbidden", "unknown-action", "action-busy", "action-failed", "device-busy", "unknown-session"];
|
|
15
15
|
type ErrorCode = (typeof ERROR_CODES)[number];
|
|
16
16
|
type RequestId = number | string;
|
|
17
17
|
/** Spends a single-use pairing token; the result carries the new device token. */
|
|
@@ -93,6 +93,9 @@ type StatsResult = Record<string, unknown>;
|
|
|
93
93
|
/** `stim settings --json`; the CLI masks sensitive values. */
|
|
94
94
|
type SettingsResult = Record<string, unknown>;
|
|
95
95
|
type Platform = "ios" | "android";
|
|
96
|
+
/** `reload` also reaches the workspace's Stim-owned Chrome page. */
|
|
97
|
+
declare const RELOAD_PLATFORMS: readonly ["ios", "android", "web"];
|
|
98
|
+
type ReloadPlatform = (typeof RELOAD_PLATFORMS)[number];
|
|
96
99
|
declare const FRAME_FPS: {
|
|
97
100
|
readonly default: 5;
|
|
98
101
|
readonly max: 30;
|
|
@@ -122,7 +125,7 @@ declare const VIDEO_CODECS: readonly ["h264"];
|
|
|
122
125
|
type VideoCodec = (typeof VIDEO_CODECS)[number];
|
|
123
126
|
/**
|
|
124
127
|
* With `video`, frames arrive as binary WebSocket messages, one H.264 access unit each; see {@link VideoPacket}.
|
|
125
|
-
* A subscription whose device
|
|
128
|
+
* A subscription whose device falls back to screenshots still sends JSON `frame` events.
|
|
126
129
|
*/
|
|
127
130
|
interface FramesSubscribeResult extends SubscribeResult {
|
|
128
131
|
video?: VideoCodec;
|
|
@@ -134,9 +137,12 @@ interface KeyframeParams {
|
|
|
134
137
|
declare const VIDEO_HEADER_VERSION = 1;
|
|
135
138
|
/** The keyframe bit of a {@link VideoPacket}'s flags. */
|
|
136
139
|
declare const VIDEO_KEYFRAME = 1;
|
|
140
|
+
/** The flag bits of a {@link VideoPacket} that carry an iPhone Duo's posture, as `posture` on a `frame` event. */
|
|
141
|
+
declare const VIDEO_FOLDED = 2;
|
|
142
|
+
declare const VIDEO_UNFOLDED = 4;
|
|
137
143
|
/**
|
|
138
144
|
* The layout of a binary video message, big-endian: u8 version ({@link VIDEO_HEADER_VERSION}), u8 flags
|
|
139
|
-
* ({@link VIDEO_KEYFRAME}), u16 header length, u32 sequence number of the messages sent on this subscription, f64 capture time in milliseconds since the
|
|
145
|
+
* ({@link VIDEO_KEYFRAME}, and on an iPhone Duo {@link VIDEO_FOLDED} or {@link VIDEO_UNFOLDED}), u16 header length, u32 sequence number of the messages sent on this subscription, f64 capture time in milliseconds since the
|
|
140
146
|
* epoch on the Mac's clock, u16 width, u16 height, u8 subscription id length N, N bytes of ASCII subscription
|
|
141
147
|
* id. After the header comes one Annex-B H.264 access unit; a keyframe carries its SPS and PPS. The stream has
|
|
142
148
|
* no B-frames, so each access unit is shown as it arrives.
|
|
@@ -148,19 +154,20 @@ interface VideoPacket {
|
|
|
148
154
|
capturedAt: number;
|
|
149
155
|
width: number;
|
|
150
156
|
height: number;
|
|
157
|
+
posture?: "folded" | "unfolded";
|
|
151
158
|
accessUnit: Uint8Array;
|
|
152
159
|
}
|
|
153
160
|
/** Each action runs one fixed `stim` command in the workspace. */
|
|
154
161
|
declare const ACTIONS: readonly ["reload", "stop"];
|
|
155
162
|
type ActionName = (typeof ACTIONS)[number];
|
|
156
163
|
/**
|
|
157
|
-
* `reload` runs `stim reload --json`, with `platform` when
|
|
164
|
+
* `reload` runs `stim reload --json`, with `platform` when more than one platform is live; `stop` runs
|
|
158
165
|
* `stim stop --json`. `workspace` is an environment `path` from a status payload. Needs `control`.
|
|
159
166
|
*/
|
|
160
167
|
type ActionParams = {
|
|
161
168
|
action: "reload";
|
|
162
169
|
workspace: string;
|
|
163
|
-
platform?:
|
|
170
|
+
platform?: ReloadPlatform;
|
|
164
171
|
} | {
|
|
165
172
|
action: "stop";
|
|
166
173
|
workspace: string;
|
|
@@ -171,6 +178,72 @@ interface ActionResult {
|
|
|
171
178
|
workspace: string;
|
|
172
179
|
output: Record<string, unknown>;
|
|
173
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Starts a control session on the device `stim status` lists as owned by `workspace` in `slot`. Needs
|
|
183
|
+
* `control`. Refused with `device-busy` while an agent, a device lock or another client drives the device,
|
|
184
|
+
* unless `takeOver` is true.
|
|
185
|
+
*/
|
|
186
|
+
interface ControlBeginParams {
|
|
187
|
+
workspace: string;
|
|
188
|
+
platform: Platform;
|
|
189
|
+
slot?: string;
|
|
190
|
+
takeOver?: boolean;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* `lease` is the `stim device lock` lease the server holds for the session, or null when it holds none, such
|
|
194
|
+
* as after taking over a device another workspace leases. `postures` lists what `input.posture` accepts for
|
|
195
|
+
* the device: `folded` and `unfolded` for an iPhone Duo, all three for an emulator with a hinge, and none
|
|
196
|
+
* otherwise.
|
|
197
|
+
*/
|
|
198
|
+
interface ControlBeginResult {
|
|
199
|
+
session: string;
|
|
200
|
+
platform: Platform;
|
|
201
|
+
lease: {
|
|
202
|
+
grantedAt: string | null;
|
|
203
|
+
expiresAt: string;
|
|
204
|
+
} | null;
|
|
205
|
+
postures: DevicePosture[];
|
|
206
|
+
}
|
|
207
|
+
interface ControlEndParams {
|
|
208
|
+
session: string;
|
|
209
|
+
}
|
|
210
|
+
declare const TOUCH_PHASES: readonly ["down", "move", "up"];
|
|
211
|
+
type TouchPhase = (typeof TOUCH_PHASES)[number];
|
|
212
|
+
/** `x` and `y` are fractions of the upright screen, origin top-left. `display` is 0 for the main display. */
|
|
213
|
+
interface InputTouchParams {
|
|
214
|
+
session: string;
|
|
215
|
+
phase: TouchPhase;
|
|
216
|
+
x: number;
|
|
217
|
+
y: number;
|
|
218
|
+
display?: number;
|
|
219
|
+
}
|
|
220
|
+
declare const MAX_INPUT_TEXT = 256;
|
|
221
|
+
/** Printable ASCII, where `\n` presses Return, `\t` Tab and `\b` Delete. */
|
|
222
|
+
interface InputTextParams {
|
|
223
|
+
session: string;
|
|
224
|
+
text: string;
|
|
225
|
+
}
|
|
226
|
+
/** `home` and `lock` on both platforms; `back` and `app-switch` on Android only. */
|
|
227
|
+
declare const INPUT_BUTTONS: readonly ["home", "lock", "back", "app-switch"];
|
|
228
|
+
type InputButton = (typeof INPUT_BUTTONS)[number];
|
|
229
|
+
interface InputButtonParams {
|
|
230
|
+
session: string;
|
|
231
|
+
button: InputButton;
|
|
232
|
+
}
|
|
233
|
+
/** `left` turns the device a quarter turn counterclockwise, `right` clockwise. */
|
|
234
|
+
declare const ROTATE_DIRECTIONS: readonly ["left", "right"];
|
|
235
|
+
type RotateDirection = (typeof ROTATE_DIRECTIONS)[number];
|
|
236
|
+
interface InputRotateParams {
|
|
237
|
+
session: string;
|
|
238
|
+
direction: RotateDirection;
|
|
239
|
+
}
|
|
240
|
+
declare const DEVICE_POSTURES: readonly ["folded", "half-open", "unfolded"];
|
|
241
|
+
type DevicePosture = (typeof DEVICE_POSTURES)[number];
|
|
242
|
+
/** Moves the hinge of a device whose `control.begin` result lists `posture`. */
|
|
243
|
+
interface InputPostureParams {
|
|
244
|
+
session: string;
|
|
245
|
+
posture: DevicePosture;
|
|
246
|
+
}
|
|
174
247
|
/** Predicts the next `ios` or `android` build of `workspace` in `slot` (`default` when absent). */
|
|
175
248
|
interface BuildPlanParams {
|
|
176
249
|
workspace: string;
|
|
@@ -292,6 +365,34 @@ interface Methods {
|
|
|
292
365
|
params: ActionParams;
|
|
293
366
|
result: ActionResult;
|
|
294
367
|
};
|
|
368
|
+
"control.begin": {
|
|
369
|
+
params: ControlBeginParams;
|
|
370
|
+
result: ControlBeginResult;
|
|
371
|
+
};
|
|
372
|
+
"control.end": {
|
|
373
|
+
params: ControlEndParams;
|
|
374
|
+
result: Record<string, never>;
|
|
375
|
+
};
|
|
376
|
+
"input.touch": {
|
|
377
|
+
params: InputTouchParams;
|
|
378
|
+
result: Record<string, never>;
|
|
379
|
+
};
|
|
380
|
+
"input.text": {
|
|
381
|
+
params: InputTextParams;
|
|
382
|
+
result: Record<string, never>;
|
|
383
|
+
};
|
|
384
|
+
"input.button": {
|
|
385
|
+
params: InputButtonParams;
|
|
386
|
+
result: Record<string, never>;
|
|
387
|
+
};
|
|
388
|
+
"input.rotate": {
|
|
389
|
+
params: InputRotateParams;
|
|
390
|
+
result: Record<string, never>;
|
|
391
|
+
};
|
|
392
|
+
"input.posture": {
|
|
393
|
+
params: InputPostureParams;
|
|
394
|
+
result: Record<string, never>;
|
|
395
|
+
};
|
|
295
396
|
}
|
|
296
397
|
type ClientRequest = { [M in Method]: {
|
|
297
398
|
id: RequestId;
|
|
@@ -357,10 +458,21 @@ interface FrameDelayedEvent {
|
|
|
357
458
|
subscription: string;
|
|
358
459
|
delayed: boolean;
|
|
359
460
|
}
|
|
360
|
-
|
|
461
|
+
declare const CONTROL_END_REASONS: readonly ["idle", "taken-over", "device-gone", "forbidden", "failed"];
|
|
462
|
+
/**
|
|
463
|
+
* The server ended a control session: no input for 5 minutes, another client took the device over, the device
|
|
464
|
+
* stopped or changed owner, the device lost `control`, or input could not reach the device.
|
|
465
|
+
*/
|
|
466
|
+
interface ControlEndedEvent {
|
|
467
|
+
event: "control-ended";
|
|
468
|
+
session: string;
|
|
469
|
+
reason: (typeof CONTROL_END_REASONS)[number];
|
|
470
|
+
message: string;
|
|
471
|
+
}
|
|
472
|
+
type ServerEvent = StatusEvent | LogsEvent | FrameEvent | FrameDelayedEvent | ErrorEvent | ControlEndedEvent;
|
|
361
473
|
type ServerMessage = ServerResponse | ServerEvent;
|
|
362
474
|
type JsonSchema = Record<string, unknown>;
|
|
363
475
|
/** The JSON Schema of every message on the wire, written to `dist/protocol.schema.json` at build time. */
|
|
364
476
|
declare function protocolJsonSchema(): JsonSchema;
|
|
365
477
|
//#endregion
|
|
366
|
-
export { ACTIONS, ActionName, ActionParams, ActionResult, BuildPlanParams, BuildPlanResult, CAPABILITIES, Capability, ClientRequest, DeviceAuth, ERROR_CODES, ErrorCode, ErrorEvent, FRAME_EDGE, FRAME_FPS, FrameDelayedEvent, FrameEvent, FrameTarget, FramesSubscribeResult, HelloParams, HelloResult, KeyframeParams, LOG_LEVELS, LOG_SOURCES, LogFilter, LogLevel, LogRecord, LogSource, LogsEvent, LogsQueryResult, MAX_LOG_TAIL, METHODS, MachineHistory, MachineHistoryParams, MachineUsage, MachineVolume, MemoryPressure, Method, Methods, PROTOCOL_SCHEMA_FILE, PROTOCOL_VERSION, PairingAuth, Platform, ProtocolError, RequestId, ServerEvent, ServerMessage, ServerResponse, SettingsResult, StatsResult, StatusEvent, SubscribeResult, UnsubscribeParams, UsageSample, VIDEO_CODECS, VIDEO_HEADER_VERSION, VIDEO_KEYFRAME, VideoCodec, VideoPacket, WorkspaceParams, protocolJsonSchema };
|
|
478
|
+
export { ACTIONS, ActionName, ActionParams, ActionResult, BuildPlanParams, BuildPlanResult, CAPABILITIES, CONTROL_END_REASONS, Capability, ClientRequest, ControlBeginParams, ControlBeginResult, ControlEndParams, ControlEndedEvent, DEVICE_POSTURES, DeviceAuth, DevicePosture, ERROR_CODES, ErrorCode, ErrorEvent, FRAME_EDGE, FRAME_FPS, FrameDelayedEvent, FrameEvent, FrameTarget, FramesSubscribeResult, HelloParams, HelloResult, INPUT_BUTTONS, InputButton, InputButtonParams, InputPostureParams, InputRotateParams, InputTextParams, InputTouchParams, KeyframeParams, LOG_LEVELS, LOG_SOURCES, LogFilter, LogLevel, LogRecord, LogSource, LogsEvent, LogsQueryResult, MAX_INPUT_TEXT, MAX_LOG_TAIL, METHODS, MachineHistory, MachineHistoryParams, MachineUsage, MachineVolume, MemoryPressure, Method, Methods, PROTOCOL_SCHEMA_FILE, PROTOCOL_VERSION, PairingAuth, Platform, ProtocolError, RELOAD_PLATFORMS, ROTATE_DIRECTIONS, ReloadPlatform, RequestId, RotateDirection, ServerEvent, ServerMessage, ServerResponse, SettingsResult, StatsResult, StatusEvent, SubscribeResult, TOUCH_PHASES, TouchPhase, UnsubscribeParams, UsageSample, VIDEO_CODECS, VIDEO_FOLDED, VIDEO_HEADER_VERSION, VIDEO_KEYFRAME, VIDEO_UNFOLDED, VideoCodec, VideoPacket, WorkspaceParams, protocolJsonSchema };
|
package/dist/protocol.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { ACTIONS, CAPABILITIES, ERROR_CODES, FRAME_EDGE, FRAME_FPS, LOG_LEVELS, LOG_SOURCES, MAX_LOG_TAIL, METHODS, PROTOCOL_SCHEMA_FILE, PROTOCOL_VERSION, VIDEO_CODECS, VIDEO_HEADER_VERSION, VIDEO_KEYFRAME, protocolJsonSchema };
|
|
1
|
+
import { C as VIDEO_UNFOLDED, S as VIDEO_KEYFRAME, _ as ROTATE_DIRECTIONS, a as ERROR_CODES, b as VIDEO_FOLDED, c as INPUT_BUTTONS, d as MAX_INPUT_TEXT, f as MAX_LOG_TAIL, g as RELOAD_PLATFORMS, h as PROTOCOL_VERSION, i as DEVICE_POSTURES, l as LOG_LEVELS, m as PROTOCOL_SCHEMA_FILE, n as CAPABILITIES, o as FRAME_EDGE, p as METHODS, r as CONTROL_END_REASONS, s as FRAME_FPS, t as ACTIONS, u as LOG_SOURCES, v as TOUCH_PHASES, w as protocolJsonSchema, x as VIDEO_HEADER_VERSION, y as VIDEO_CODECS } from "./protocol-rC-5KT1O.mjs";
|
|
2
|
+
export { ACTIONS, CAPABILITIES, CONTROL_END_REASONS, DEVICE_POSTURES, ERROR_CODES, FRAME_EDGE, FRAME_FPS, INPUT_BUTTONS, LOG_LEVELS, LOG_SOURCES, MAX_INPUT_TEXT, MAX_LOG_TAIL, METHODS, PROTOCOL_SCHEMA_FILE, PROTOCOL_VERSION, RELOAD_PLATFORMS, ROTATE_DIRECTIONS, TOUCH_PHASES, VIDEO_CODECS, VIDEO_FOLDED, VIDEO_HEADER_VERSION, VIDEO_KEYFRAME, VIDEO_UNFOLDED, protocolJsonSchema };
|