@stim-cli/server 1.11.0 → 1.12.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 +82 -5
- package/dist/{protocol-CmlG7X7_.mjs → protocol-BElQc2vm.mjs} +159 -5
- package/dist/protocol.d.mts +109 -4
- package/dist/protocol.mjs +2 -2
- package/dist/protocol.schema.json +399 -2
- 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 +261 -27
- package/dist/stim-frames/sim-fold.entitlements +8 -0
- package/dist/stim-frames/sim-fold.m +47 -0
- package/dist/stim-server.mjs +1011 -155
- package/package.json +3 -3
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"forbidden",
|
|
33
33
|
"unknown-action",
|
|
34
34
|
"action-busy",
|
|
35
|
-
"action-failed"
|
|
35
|
+
"action-failed",
|
|
36
|
+
"device-busy",
|
|
37
|
+
"unknown-session"
|
|
36
38
|
]
|
|
37
39
|
},
|
|
38
40
|
"message": {
|
|
@@ -232,10 +234,11 @@
|
|
|
232
234
|
},
|
|
233
235
|
"slot": {
|
|
234
236
|
"type": "string",
|
|
235
|
-
"
|
|
237
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$"
|
|
236
238
|
},
|
|
237
239
|
"grep": {
|
|
238
240
|
"type": "string",
|
|
241
|
+
"pattern": "^[^\\u0000]*$",
|
|
239
242
|
"description": "A regular expression matched against each message."
|
|
240
243
|
},
|
|
241
244
|
"errors": {
|
|
@@ -363,6 +366,95 @@
|
|
|
363
366
|
}
|
|
364
367
|
}
|
|
365
368
|
},
|
|
369
|
+
"ControlBeginParams": {
|
|
370
|
+
"type": "object",
|
|
371
|
+
"required": [
|
|
372
|
+
"workspace",
|
|
373
|
+
"platform"
|
|
374
|
+
],
|
|
375
|
+
"additionalProperties": false,
|
|
376
|
+
"properties": {
|
|
377
|
+
"workspace": {
|
|
378
|
+
"type": "string",
|
|
379
|
+
"description": "An environment path from a status payload."
|
|
380
|
+
},
|
|
381
|
+
"platform": {
|
|
382
|
+
"enum": [
|
|
383
|
+
"ios",
|
|
384
|
+
"android"
|
|
385
|
+
]
|
|
386
|
+
},
|
|
387
|
+
"slot": {
|
|
388
|
+
"type": "string",
|
|
389
|
+
"minLength": 1,
|
|
390
|
+
"default": "default"
|
|
391
|
+
},
|
|
392
|
+
"takeOver": {
|
|
393
|
+
"type": "boolean",
|
|
394
|
+
"default": false
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
"ControlBeginResult": {
|
|
399
|
+
"type": "object",
|
|
400
|
+
"required": [
|
|
401
|
+
"session",
|
|
402
|
+
"platform",
|
|
403
|
+
"lease",
|
|
404
|
+
"postures"
|
|
405
|
+
],
|
|
406
|
+
"additionalProperties": false,
|
|
407
|
+
"properties": {
|
|
408
|
+
"session": {
|
|
409
|
+
"type": "string"
|
|
410
|
+
},
|
|
411
|
+
"platform": {
|
|
412
|
+
"enum": [
|
|
413
|
+
"ios",
|
|
414
|
+
"android"
|
|
415
|
+
]
|
|
416
|
+
},
|
|
417
|
+
"lease": {
|
|
418
|
+
"oneOf": [
|
|
419
|
+
{
|
|
420
|
+
"type": "null"
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
"type": "object",
|
|
424
|
+
"required": [
|
|
425
|
+
"grantedAt",
|
|
426
|
+
"expiresAt"
|
|
427
|
+
],
|
|
428
|
+
"additionalProperties": false,
|
|
429
|
+
"properties": {
|
|
430
|
+
"grantedAt": {
|
|
431
|
+
"type": [
|
|
432
|
+
"string",
|
|
433
|
+
"null"
|
|
434
|
+
],
|
|
435
|
+
"format": "date-time"
|
|
436
|
+
},
|
|
437
|
+
"expiresAt": {
|
|
438
|
+
"type": "string",
|
|
439
|
+
"format": "date-time"
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
]
|
|
444
|
+
},
|
|
445
|
+
"postures": {
|
|
446
|
+
"type": "array",
|
|
447
|
+
"items": {
|
|
448
|
+
"enum": [
|
|
449
|
+
"folded",
|
|
450
|
+
"half-open",
|
|
451
|
+
"unfolded"
|
|
452
|
+
]
|
|
453
|
+
},
|
|
454
|
+
"uniqueItems": true
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
},
|
|
366
458
|
"BuildPlanParams": {
|
|
367
459
|
"type": "object",
|
|
368
460
|
"required": [
|
|
@@ -895,6 +987,278 @@
|
|
|
895
987
|
"$ref": "#/$defs/ActionParams"
|
|
896
988
|
}
|
|
897
989
|
}
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
"type": "object",
|
|
993
|
+
"required": [
|
|
994
|
+
"id",
|
|
995
|
+
"method",
|
|
996
|
+
"params"
|
|
997
|
+
],
|
|
998
|
+
"additionalProperties": false,
|
|
999
|
+
"properties": {
|
|
1000
|
+
"id": {
|
|
1001
|
+
"type": [
|
|
1002
|
+
"integer",
|
|
1003
|
+
"string"
|
|
1004
|
+
]
|
|
1005
|
+
},
|
|
1006
|
+
"method": {
|
|
1007
|
+
"const": "control.begin"
|
|
1008
|
+
},
|
|
1009
|
+
"params": {
|
|
1010
|
+
"$ref": "#/$defs/ControlBeginParams"
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
"type": "object",
|
|
1016
|
+
"required": [
|
|
1017
|
+
"id",
|
|
1018
|
+
"method",
|
|
1019
|
+
"params"
|
|
1020
|
+
],
|
|
1021
|
+
"additionalProperties": false,
|
|
1022
|
+
"properties": {
|
|
1023
|
+
"id": {
|
|
1024
|
+
"type": [
|
|
1025
|
+
"integer",
|
|
1026
|
+
"string"
|
|
1027
|
+
]
|
|
1028
|
+
},
|
|
1029
|
+
"method": {
|
|
1030
|
+
"const": "control.end"
|
|
1031
|
+
},
|
|
1032
|
+
"params": {
|
|
1033
|
+
"type": "object",
|
|
1034
|
+
"required": [
|
|
1035
|
+
"session"
|
|
1036
|
+
],
|
|
1037
|
+
"additionalProperties": false,
|
|
1038
|
+
"properties": {
|
|
1039
|
+
"session": {
|
|
1040
|
+
"type": "string"
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
"type": "object",
|
|
1048
|
+
"required": [
|
|
1049
|
+
"id",
|
|
1050
|
+
"method",
|
|
1051
|
+
"params"
|
|
1052
|
+
],
|
|
1053
|
+
"additionalProperties": false,
|
|
1054
|
+
"properties": {
|
|
1055
|
+
"id": {
|
|
1056
|
+
"type": [
|
|
1057
|
+
"integer",
|
|
1058
|
+
"string"
|
|
1059
|
+
]
|
|
1060
|
+
},
|
|
1061
|
+
"method": {
|
|
1062
|
+
"const": "input.touch"
|
|
1063
|
+
},
|
|
1064
|
+
"params": {
|
|
1065
|
+
"type": "object",
|
|
1066
|
+
"required": [
|
|
1067
|
+
"session",
|
|
1068
|
+
"phase",
|
|
1069
|
+
"x",
|
|
1070
|
+
"y"
|
|
1071
|
+
],
|
|
1072
|
+
"additionalProperties": false,
|
|
1073
|
+
"properties": {
|
|
1074
|
+
"session": {
|
|
1075
|
+
"type": "string"
|
|
1076
|
+
},
|
|
1077
|
+
"phase": {
|
|
1078
|
+
"enum": [
|
|
1079
|
+
"down",
|
|
1080
|
+
"move",
|
|
1081
|
+
"up"
|
|
1082
|
+
]
|
|
1083
|
+
},
|
|
1084
|
+
"x": {
|
|
1085
|
+
"type": "number",
|
|
1086
|
+
"minimum": 0,
|
|
1087
|
+
"maximum": 1
|
|
1088
|
+
},
|
|
1089
|
+
"y": {
|
|
1090
|
+
"type": "number",
|
|
1091
|
+
"minimum": 0,
|
|
1092
|
+
"maximum": 1
|
|
1093
|
+
},
|
|
1094
|
+
"display": {
|
|
1095
|
+
"type": "integer",
|
|
1096
|
+
"minimum": 0,
|
|
1097
|
+
"maximum": 3,
|
|
1098
|
+
"default": 0
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
"type": "object",
|
|
1106
|
+
"required": [
|
|
1107
|
+
"id",
|
|
1108
|
+
"method",
|
|
1109
|
+
"params"
|
|
1110
|
+
],
|
|
1111
|
+
"additionalProperties": false,
|
|
1112
|
+
"properties": {
|
|
1113
|
+
"id": {
|
|
1114
|
+
"type": [
|
|
1115
|
+
"integer",
|
|
1116
|
+
"string"
|
|
1117
|
+
]
|
|
1118
|
+
},
|
|
1119
|
+
"method": {
|
|
1120
|
+
"const": "input.text"
|
|
1121
|
+
},
|
|
1122
|
+
"params": {
|
|
1123
|
+
"type": "object",
|
|
1124
|
+
"required": [
|
|
1125
|
+
"session",
|
|
1126
|
+
"text"
|
|
1127
|
+
],
|
|
1128
|
+
"additionalProperties": false,
|
|
1129
|
+
"properties": {
|
|
1130
|
+
"session": {
|
|
1131
|
+
"type": "string"
|
|
1132
|
+
},
|
|
1133
|
+
"text": {
|
|
1134
|
+
"type": "string",
|
|
1135
|
+
"minLength": 1,
|
|
1136
|
+
"maxLength": 256,
|
|
1137
|
+
"pattern": "^[\\x20-\\x7e\\n\\t\\b]+$"
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
"type": "object",
|
|
1145
|
+
"required": [
|
|
1146
|
+
"id",
|
|
1147
|
+
"method",
|
|
1148
|
+
"params"
|
|
1149
|
+
],
|
|
1150
|
+
"additionalProperties": false,
|
|
1151
|
+
"properties": {
|
|
1152
|
+
"id": {
|
|
1153
|
+
"type": [
|
|
1154
|
+
"integer",
|
|
1155
|
+
"string"
|
|
1156
|
+
]
|
|
1157
|
+
},
|
|
1158
|
+
"method": {
|
|
1159
|
+
"const": "input.button"
|
|
1160
|
+
},
|
|
1161
|
+
"params": {
|
|
1162
|
+
"type": "object",
|
|
1163
|
+
"required": [
|
|
1164
|
+
"session",
|
|
1165
|
+
"button"
|
|
1166
|
+
],
|
|
1167
|
+
"additionalProperties": false,
|
|
1168
|
+
"properties": {
|
|
1169
|
+
"session": {
|
|
1170
|
+
"type": "string"
|
|
1171
|
+
},
|
|
1172
|
+
"button": {
|
|
1173
|
+
"enum": [
|
|
1174
|
+
"home",
|
|
1175
|
+
"lock",
|
|
1176
|
+
"back",
|
|
1177
|
+
"app-switch"
|
|
1178
|
+
]
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
"type": "object",
|
|
1186
|
+
"required": [
|
|
1187
|
+
"id",
|
|
1188
|
+
"method",
|
|
1189
|
+
"params"
|
|
1190
|
+
],
|
|
1191
|
+
"additionalProperties": false,
|
|
1192
|
+
"properties": {
|
|
1193
|
+
"id": {
|
|
1194
|
+
"type": [
|
|
1195
|
+
"integer",
|
|
1196
|
+
"string"
|
|
1197
|
+
]
|
|
1198
|
+
},
|
|
1199
|
+
"method": {
|
|
1200
|
+
"const": "input.rotate"
|
|
1201
|
+
},
|
|
1202
|
+
"params": {
|
|
1203
|
+
"type": "object",
|
|
1204
|
+
"required": [
|
|
1205
|
+
"session",
|
|
1206
|
+
"direction"
|
|
1207
|
+
],
|
|
1208
|
+
"additionalProperties": false,
|
|
1209
|
+
"properties": {
|
|
1210
|
+
"session": {
|
|
1211
|
+
"type": "string"
|
|
1212
|
+
},
|
|
1213
|
+
"direction": {
|
|
1214
|
+
"enum": [
|
|
1215
|
+
"left",
|
|
1216
|
+
"right"
|
|
1217
|
+
]
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
"type": "object",
|
|
1225
|
+
"required": [
|
|
1226
|
+
"id",
|
|
1227
|
+
"method",
|
|
1228
|
+
"params"
|
|
1229
|
+
],
|
|
1230
|
+
"additionalProperties": false,
|
|
1231
|
+
"properties": {
|
|
1232
|
+
"id": {
|
|
1233
|
+
"type": [
|
|
1234
|
+
"integer",
|
|
1235
|
+
"string"
|
|
1236
|
+
]
|
|
1237
|
+
},
|
|
1238
|
+
"method": {
|
|
1239
|
+
"const": "input.posture"
|
|
1240
|
+
},
|
|
1241
|
+
"params": {
|
|
1242
|
+
"type": "object",
|
|
1243
|
+
"required": [
|
|
1244
|
+
"session",
|
|
1245
|
+
"posture"
|
|
1246
|
+
],
|
|
1247
|
+
"additionalProperties": false,
|
|
1248
|
+
"properties": {
|
|
1249
|
+
"session": {
|
|
1250
|
+
"type": "string"
|
|
1251
|
+
},
|
|
1252
|
+
"posture": {
|
|
1253
|
+
"enum": [
|
|
1254
|
+
"folded",
|
|
1255
|
+
"half-open",
|
|
1256
|
+
"unfolded"
|
|
1257
|
+
]
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
898
1262
|
}
|
|
899
1263
|
]
|
|
900
1264
|
},
|
|
@@ -919,6 +1283,9 @@
|
|
|
919
1283
|
{
|
|
920
1284
|
"$ref": "#/$defs/HelloResult"
|
|
921
1285
|
},
|
|
1286
|
+
{
|
|
1287
|
+
"$ref": "#/$defs/ControlBeginResult"
|
|
1288
|
+
},
|
|
922
1289
|
{
|
|
923
1290
|
"$ref": "#/$defs/ActionResult"
|
|
924
1291
|
},
|
|
@@ -1111,6 +1478,36 @@
|
|
|
1111
1478
|
}
|
|
1112
1479
|
}
|
|
1113
1480
|
},
|
|
1481
|
+
{
|
|
1482
|
+
"type": "object",
|
|
1483
|
+
"required": [
|
|
1484
|
+
"event",
|
|
1485
|
+
"session",
|
|
1486
|
+
"reason",
|
|
1487
|
+
"message"
|
|
1488
|
+
],
|
|
1489
|
+
"additionalProperties": false,
|
|
1490
|
+
"properties": {
|
|
1491
|
+
"event": {
|
|
1492
|
+
"const": "control-ended"
|
|
1493
|
+
},
|
|
1494
|
+
"session": {
|
|
1495
|
+
"type": "string"
|
|
1496
|
+
},
|
|
1497
|
+
"reason": {
|
|
1498
|
+
"enum": [
|
|
1499
|
+
"idle",
|
|
1500
|
+
"taken-over",
|
|
1501
|
+
"device-gone",
|
|
1502
|
+
"forbidden",
|
|
1503
|
+
"failed"
|
|
1504
|
+
]
|
|
1505
|
+
},
|
|
1506
|
+
"message": {
|
|
1507
|
+
"type": "string"
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
},
|
|
1114
1511
|
{
|
|
1115
1512
|
"type": "object",
|
|
1116
1513
|
"required": [
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// The posture of a foldable emulator, as the emulator's Posture.PostureValue
|
|
4
|
+
/// numbers it.
|
|
5
|
+
public enum EmulatorPosture: Int, CaseIterable, Sendable {
|
|
6
|
+
case closed = 1
|
|
7
|
+
case halfOpened = 2
|
|
8
|
+
case opened = 3
|
|
9
|
+
case flipped = 4
|
|
10
|
+
case tent = 5
|
|
11
|
+
|
|
12
|
+
public var label: String {
|
|
13
|
+
switch self {
|
|
14
|
+
case .closed: return "Folded"
|
|
15
|
+
case .halfOpened: return "Half open"
|
|
16
|
+
case .opened: return "Unfolded"
|
|
17
|
+
case .flipped: return "Flipped"
|
|
18
|
+
case .tent: return "Tent"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// The emulator's current posture; nil for an emulator without a hinge, or
|
|
23
|
+
/// one whose endpoint cannot be reached.
|
|
24
|
+
public static func current(serial: String) async -> EmulatorPosture? {
|
|
25
|
+
guard let endpoint = EmulatorDiscovery.endpoint(serial: serial) else { return nil }
|
|
26
|
+
let input = EmulatorInput(endpoint: endpoint)
|
|
27
|
+
defer { input.close() }
|
|
28
|
+
return await input.call("getPhysicalModel", PostureMessages.postureTarget).flatMap(PostureMessages.posture)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Moves the hinge to this posture. Returns false when the emulator has no
|
|
32
|
+
/// endpoint or refuses the call.
|
|
33
|
+
public func apply(serial: String) async -> Bool {
|
|
34
|
+
guard let endpoint = EmulatorDiscovery.endpoint(serial: serial) else { return false }
|
|
35
|
+
let input = EmulatorInput(endpoint: endpoint)
|
|
36
|
+
defer { input.close() }
|
|
37
|
+
return await input.call("setPosture", PostureMessages.setPosture(self)) != nil
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Field numbers follow PhysicalModelValue, ParameterValue and Posture in the
|
|
42
|
+
// emulator's emulator_controller.proto (sdk/emulator/lib); POSTURE is
|
|
43
|
+
// PhysicalType 16, and its single value is the PostureValue as a float.
|
|
44
|
+
enum PostureMessages {
|
|
45
|
+
static let postureTarget = Data([0x08, 0x10])
|
|
46
|
+
|
|
47
|
+
static func setPosture(_ posture: EmulatorPosture) -> Data {
|
|
48
|
+
Data([0x18, UInt8(posture.rawValue)])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static func posture(fromPhysicalModel bytes: Data) -> EmulatorPosture? {
|
|
52
|
+
var reader = ProtoReader(bytes)
|
|
53
|
+
while let (field, value) = reader.next() {
|
|
54
|
+
if field == 2, case .varint(let status) = value, status != 0 { return nil }
|
|
55
|
+
guard field == 3, case .bytes(let parameter) = value else { continue }
|
|
56
|
+
var values = ProtoReader(parameter)
|
|
57
|
+
while let (valueField, packed) = values.next() {
|
|
58
|
+
guard valueField == 1, case .bytes(let data) = packed, data.count >= 4 else { continue }
|
|
59
|
+
let raw = Float(bitPattern: data.prefix(4).reversed().reduce(0) { $0 << 8 | UInt32($1) })
|
|
60
|
+
return EmulatorPosture(rawValue: Int(raw.rounded()))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return nil
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
extension EmulatorInput {
|
|
68
|
+
func call(_ method: String, _ message: Data) async -> Data? {
|
|
69
|
+
await withCheckedContinuation { continuation in
|
|
70
|
+
call(method, message) { continuation.resume(returning: $0) }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Turns a running emulator a quarter turn through its gRPC endpoint. It sets
|
|
4
|
+
/// the z angle of the ROTATION physical model, as turning a real device would,
|
|
5
|
+
/// so the screen follows the app's and the system's rotation rules; with
|
|
6
|
+
/// auto-rotate off, Android offers its rotate suggestion instead.
|
|
7
|
+
public enum EmulatorRotation {
|
|
8
|
+
/// Returns false when the emulator has no endpoint or refuses the call.
|
|
9
|
+
public static func rotate(serial: String, clockwise: Bool) async -> Bool {
|
|
10
|
+
guard let endpoint = EmulatorDiscovery.endpoint(serial: serial) else { return false }
|
|
11
|
+
let input = EmulatorInput(endpoint: endpoint)
|
|
12
|
+
defer { input.close() }
|
|
13
|
+
guard let current = await input.call("getPhysicalModel", RotationMessages.rotationTarget) else { return false }
|
|
14
|
+
let angles = RotationMessages.angles(fromPhysicalModel: current)
|
|
15
|
+
let turned = RotationMessages.quarterTurn(z: angles.z, clockwise: clockwise)
|
|
16
|
+
let message = RotationMessages.rotation(x: angles.x, y: angles.y, z: turned)
|
|
17
|
+
return await input.call("setPhysicalModel", message) != nil
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Field numbers follow PhysicalModelValue and ParameterValue in the emulator's
|
|
22
|
+
// emulator_controller.proto (sdk/emulator/lib); ROTATION is PhysicalType 1 and
|
|
23
|
+
// its values are x, y and z angles in degrees.
|
|
24
|
+
enum RotationMessages {
|
|
25
|
+
static let rotationTarget = Data([0x08, 0x01])
|
|
26
|
+
|
|
27
|
+
static func rotation(x: Float, y: Float, z: Float) -> Data {
|
|
28
|
+
var floats = Data()
|
|
29
|
+
for value in [x, y, z] { withUnsafeBytes(of: value.bitPattern.littleEndian) { floats += $0 } }
|
|
30
|
+
let parameter = Data([0x0a, UInt8(floats.count)]) + floats
|
|
31
|
+
return rotationTarget + Data([0x1a, UInt8(parameter.count)]) + parameter
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// The angles in a PhysicalModelValue; proto3 omits an all-zero value.
|
|
35
|
+
static func angles(fromPhysicalModel bytes: Data) -> (x: Float, y: Float, z: Float) {
|
|
36
|
+
var reader = ProtoReader(bytes)
|
|
37
|
+
while let (field, value) = reader.next() {
|
|
38
|
+
guard field == 3, case .bytes(let parameter) = value else { continue }
|
|
39
|
+
var values = ProtoReader(parameter)
|
|
40
|
+
while let (valueField, packed) = values.next() {
|
|
41
|
+
guard valueField == 1, case .bytes(let data) = packed, data.count >= 12 else { continue }
|
|
42
|
+
let floats = stride(from: 0, to: 12, by: 4).map { offset in
|
|
43
|
+
Float(bitPattern: data.dropFirst(offset).prefix(4).reversed().reduce(0) { $0 << 8 | UInt32($1) })
|
|
44
|
+
}
|
|
45
|
+
return (floats[0], floats[1], floats[2])
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return (0, 0, 0)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// The next z angle, in (-180, 180]; a positive angle turns the device counterclockwise.
|
|
52
|
+
static func quarterTurn(z: Float, clockwise: Bool) -> Float {
|
|
53
|
+
let quarter = (z / 90).rounded() * 90
|
|
54
|
+
var next = quarter + (clockwise ? -90 : 90)
|
|
55
|
+
if next > 180 { next -= 360 }
|
|
56
|
+
if next <= -180 { next += 360 }
|
|
57
|
+
return next
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -5,6 +5,7 @@ import Foundation
|
|
|
5
5
|
enum InputMessages {
|
|
6
6
|
static let macKeyCodeType: UInt64 = 4
|
|
7
7
|
static let keyup: UInt64 = 1
|
|
8
|
+
static let keypress: UInt64 = 2
|
|
8
9
|
|
|
9
10
|
/// A MouseEvent on the main display. `pressed` sets the primary button; the
|
|
10
11
|
/// emulator turns it into a single touch on a touchscreen device.
|
|
@@ -12,6 +13,20 @@ enum InputMessages {
|
|
|
12
13
|
message([(1, UInt64(max(x, 0))), (2, UInt64(max(y, 0))), (3, pressed ? 1 : 0)])
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/// A TouchEvent with one finger on the main display, lifted when `pressed`
|
|
17
|
+
/// is false (pressure 0). Unlike a MouseEvent, Android sees a touchscreen
|
|
18
|
+
/// finger, not a mouse or stylus.
|
|
19
|
+
static func touch(x: Int, y: Int, pressed: Bool) -> Data {
|
|
20
|
+
var touch = Data()
|
|
21
|
+
for (field, value) in [(1, UInt64(max(x, 0))), (2, UInt64(max(y, 0))), (4, pressed ? 1 : 0)] where value != 0 {
|
|
22
|
+
ScreenshotMessages.appendVarint(UInt64(field << 3), to: &touch)
|
|
23
|
+
ScreenshotMessages.appendVarint(value, to: &touch)
|
|
24
|
+
}
|
|
25
|
+
var out = Data()
|
|
26
|
+
appendBytes(field: 1, touch, to: &out)
|
|
27
|
+
return out
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
/// A KeyboardEvent the emulator types as a key press per character.
|
|
16
31
|
static func text(_ text: String) -> Data {
|
|
17
32
|
var out = Data()
|
|
@@ -25,23 +40,42 @@ enum InputMessages {
|
|
|
25
40
|
message([(1, macKeyCodeType), (2, down ? 0 : keyup), (3, UInt64(macKeyCode))])
|
|
26
41
|
}
|
|
27
42
|
|
|
43
|
+
/// A KeyboardEvent that presses and releases the key a DOM key name such as
|
|
44
|
+
/// `GoHome` or `Power` names.
|
|
45
|
+
static func namedKey(_ key: String) -> Data {
|
|
46
|
+
var out = message([(2, keypress)])
|
|
47
|
+
appendBytes(field: 4, Data(key.utf8), to: &out)
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
/// The main display's size in pixels, read from `hw.lcd.width` and
|
|
29
52
|
/// `hw.lcd.height` in an EmulatorStatus's hardwareConfig.
|
|
30
53
|
static func displaySize(fromStatus bytes: Data) -> (width: Int, height: Int)? {
|
|
54
|
+
let config = hardwareConfig(fromStatus: bytes)
|
|
55
|
+
guard let width = config["hw.lcd.width"].flatMap({ Int($0) }), let height = config["hw.lcd.height"].flatMap({ Int($0) }),
|
|
56
|
+
width > 0, height > 0
|
|
57
|
+
else { return nil }
|
|
58
|
+
return (width, height)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Whether the emulator has a hardware keyboard (`hw.keyboard` in an
|
|
62
|
+
/// EmulatorStatus's hardwareConfig). Without one it drops every KeyboardEvent.
|
|
63
|
+
static func hasKeyboard(fromStatus bytes: Data) -> Bool {
|
|
64
|
+
hardwareConfig(fromStatus: bytes)["hw.keyboard"] == "true"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static func hardwareConfig(fromStatus bytes: Data) -> [String: String] {
|
|
31
68
|
var reader = ProtoReader(bytes)
|
|
32
|
-
var
|
|
33
|
-
var height: Int?
|
|
69
|
+
var config: [String: String] = [:]
|
|
34
70
|
while let (field, value) = reader.next() {
|
|
35
71
|
guard field == 5, case .bytes(let list) = value else { continue }
|
|
36
72
|
var entries = ProtoReader(list)
|
|
37
73
|
while let (entryField, entryValue) = entries.next() {
|
|
38
74
|
guard entryField == 1, case .bytes(let entry) = entryValue, let (key, value) = pair(entry) else { continue }
|
|
39
|
-
|
|
40
|
-
if key == "hw.lcd.height" { height = Int(value) }
|
|
75
|
+
config[key] = value
|
|
41
76
|
}
|
|
42
77
|
}
|
|
43
|
-
|
|
44
|
-
return (width, height)
|
|
78
|
+
return config
|
|
45
79
|
}
|
|
46
80
|
|
|
47
81
|
private static func pair(_ bytes: Data) -> (String, String)? {
|
|
@@ -71,3 +105,19 @@ enum InputMessages {
|
|
|
71
105
|
out += bytes
|
|
72
106
|
}
|
|
73
107
|
}
|
|
108
|
+
|
|
109
|
+
/// Maps a fraction of the upright image, origin top-left, to a pixel of the
|
|
110
|
+
/// display in its native orientation, which is where the emulator places
|
|
111
|
+
/// touches. `rotation` is the image's Rotation.SkinRotation.
|
|
112
|
+
func displayPixel(_ point: CGPoint, rotation: Int, displaySize: CGSize) -> (x: Int, y: Int) {
|
|
113
|
+
let native: CGPoint
|
|
114
|
+
switch rotation {
|
|
115
|
+
case 1: native = CGPoint(x: 1 - point.y, y: point.x)
|
|
116
|
+
case 2: native = CGPoint(x: 1 - point.x, y: 1 - point.y)
|
|
117
|
+
case 3: native = CGPoint(x: point.y, y: 1 - point.x)
|
|
118
|
+
default: native = point
|
|
119
|
+
}
|
|
120
|
+
let x = Int((native.x * displaySize.width).rounded(.down))
|
|
121
|
+
let y = Int((native.y * displaySize.height).rounded(.down))
|
|
122
|
+
return (min(max(x, 0), Int(displaySize.width) - 1), min(max(y, 0), Int(displaySize.height) - 1))
|
|
123
|
+
}
|