serve-sim 0.1.49 → 0.1.50-beta.145.1

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 CHANGED
@@ -54,7 +54,9 @@ serve-sim fold 100 -d <udid>
54
54
 
55
55
  Commands wait for the guest to acknowledge dispatch. Invalid angles and failed
56
56
  commands do not change capture. The bundled `simduo/serve-sim-duo-hid` executable
57
- runs inside the simulator and exits with the device session.
57
+ runs inside the simulator and exits with the device session. Its `watch-angle` mode
58
+ reads the hinge angle inside the simulator from CoreMotion, so it doesn't depend on
59
+ `devicectl`. That is how the preview learns the current angle and follows Device Hub.
58
60
 
59
61
  Duo uses one interactive RealityKit preview of the installed Xcode model, with
60
62
  live screen textures and touches mapped to each bent screen half. Apple's model
@@ -69,6 +69,7 @@ void SimCamLogSwallowedRuntimeError(NSString *via, id object, NSDictionary *user
69
69
 
70
70
  AVCaptureDeviceFormat *SimCamSharedFakeFormat(void);
71
71
  AVCaptureDevice *SimCamFakeDeviceForPosition(AVCaptureDevicePosition p);
72
+ NSArray<AVCaptureDevice *> *SimCamFakeDevices(void);
72
73
  AVCaptureDeviceInput *SimCamFakeInputForPosition(AVCaptureDevicePosition p);
73
74
  NSArray<AVCaptureInputPort *> *SimCamFakePortsForInput(AVCaptureInput *input);
74
75
 
@@ -143,6 +143,14 @@ void SimCamLogSwallowedRuntimeError(NSString *via, id object, NSDictionary *user
143
143
  - (BOOL)isVideoBinned { return NO; }
144
144
  - (BOOL)isVideoHDRSupported { return NO; }
145
145
  - (BOOL)isVideoStabilizationModeSupported:(AVCaptureVideoStabilizationMode)m { return NO; }
146
+ // AVFoundation's description reads private state this instance doesn't have.
147
+ - (NSString *)description {
148
+ CMFormatDescriptionRef format = self.formatDescription;
149
+ CMVideoDimensions size = CMVideoFormatDescriptionGetDimensions(format);
150
+ FourCharCode subtype = CMFormatDescriptionGetMediaSubType(format);
151
+ return [NSString stringWithFormat:@"<SimCamFakeFormat %dx%d %c%c%c%c>", size.width, size.height,
152
+ (char)(subtype >> 24), (char)(subtype >> 16), (char)(subtype >> 8), (char)subtype];
153
+ }
146
154
  - (CGFloat)videoMaxZoomFactor { return 16.0; }
147
155
  - (CGFloat)videoZoomFactorUpscaleThreshold { return 1.0; }
148
156
  - (AVCaptureAutoFocusSystem)autoFocusSystem { return AVCaptureAutoFocusSystemNone; }
@@ -293,6 +301,13 @@ AVCaptureDevice *SimCamFakeDeviceForPosition(AVCaptureDevicePosition p) {
293
301
  return p == AVCaptureDevicePositionBack ? back : front;
294
302
  }
295
303
 
304
+ NSArray<AVCaptureDevice *> *SimCamFakeDevices(void) {
305
+ return @[
306
+ SimCamFakeDeviceForPosition(AVCaptureDevicePositionFront),
307
+ SimCamFakeDeviceForPosition(AVCaptureDevicePositionBack),
308
+ ];
309
+ }
310
+
296
311
  #pragma mark - SimCamFakeConnection
297
312
 
298
313
  @interface AVCaptureConnection (SimCamPrivate)
@@ -129,22 +129,24 @@ BOOL SwizzleInstanceMethod(Class cls, SEL orig, SEL swiz) {
129
129
  }
130
130
  return [self simcam_defaultDeviceWithMediaType:m];
131
131
  }
132
+ + (AVCaptureDevice *)simcam_deviceWithUniqueID:(NSString *)uniqueID {
133
+ for (AVCaptureDevice *fake in SimCamFakeDevices()) {
134
+ if ([fake.uniqueID isEqualToString:uniqueID]) {
135
+ simcam_log(@"deviceWithUniqueID: %@ → fake", uniqueID);
136
+ return fake;
137
+ }
138
+ }
139
+ return [self simcam_deviceWithUniqueID:uniqueID];
140
+ }
132
141
  + (NSArray<AVCaptureDevice *> *)simcam_devicesWithMediaType:(AVMediaType)m {
133
142
  if ([m isEqualToString:AVMediaTypeVideo]) {
134
- return @[
135
- SimCamFakeDeviceForPosition(AVCaptureDevicePositionFront),
136
- SimCamFakeDeviceForPosition(AVCaptureDevicePositionBack),
137
- ];
143
+ return SimCamFakeDevices();
138
144
  }
139
145
  return [self simcam_devicesWithMediaType:m];
140
146
  }
141
147
  + (NSArray<AVCaptureDevice *> *)simcam_devices {
142
148
  NSArray *real = [self simcam_devices];
143
- NSArray *fakes = @[
144
- SimCamFakeDeviceForPosition(AVCaptureDevicePositionFront),
145
- SimCamFakeDeviceForPosition(AVCaptureDevicePositionBack),
146
- ];
147
- return [fakes arrayByAddingObjectsFromArray:real ?: @[]];
149
+ return [SimCamFakeDevices() arrayByAddingObjectsFromArray:real ?: @[]];
148
150
  }
149
151
  @end
150
152
 
@@ -160,10 +162,9 @@ BOOL SwizzleInstanceMethod(Class cls, SEL orig, SEL swiz) {
160
162
  [self simcam_discoverySessionWithDeviceTypes:types mediaType:m position:p];
161
163
  if ([m isEqualToString:AVMediaTypeVideo] || m == nil) {
162
164
  NSMutableArray *list = [NSMutableArray new];
163
- if (p == AVCaptureDevicePositionUnspecified || p == AVCaptureDevicePositionFront)
164
- [list addObject:SimCamFakeDeviceForPosition(AVCaptureDevicePositionFront)];
165
- if (p == AVCaptureDevicePositionUnspecified || p == AVCaptureDevicePositionBack)
166
- [list addObject:SimCamFakeDeviceForPosition(AVCaptureDevicePositionBack)];
165
+ for (AVCaptureDevice *fake in SimCamFakeDevices()) {
166
+ if (p == AVCaptureDevicePositionUnspecified || p == fake.position) [list addObject:fake];
167
+ }
167
168
  @try {
168
169
  [real setValue:list forKey:@"devices"];
169
170
  } @catch (__unused id e) {
@@ -1221,6 +1222,9 @@ void SimCamInstallSwizzles(void) {
1221
1222
  @selector(devicesWithMediaType:),
1222
1223
  @selector(simcam_devicesWithMediaType:));
1223
1224
  SwizzleClassMethod(dev, @selector(devices), @selector(simcam_devices));
1225
+ SwizzleClassMethod(dev,
1226
+ @selector(deviceWithUniqueID:),
1227
+ @selector(simcam_deviceWithUniqueID:));
1224
1228
 
1225
1229
  Class disc = [AVCaptureDeviceDiscoverySession class];
1226
1230
  SwizzleClassMethod(disc,
@@ -1,5 +1,6 @@
1
1
  // Guest HID transport for hinge angles, physical orientation, and hardware buttons.
2
2
  // Spawned inside iOS Simulator; one command/reply per line until EOF.
3
+ // `watch-angle` instead streams the hinge angle the guest reports (see watchAngle).
3
4
  #import <Foundation/Foundation.h>
4
5
  #import <dlfcn.h>
5
6
  #import <mach/mach_time.h>
@@ -8,6 +9,55 @@
8
9
  #import <math.h>
9
10
  #import <unistd.h>
10
11
 
12
+ // CoreMotion's private hinge-angle API, the source UIKit's UIHinge reads.
13
+ @interface CMAngle : NSObject
14
+ - (float)angleDegrees;
15
+ @end
16
+ @interface CMAngleManager : NSObject
17
+ + (BOOL)isAvailable;
18
+ - (void)startAngleUpdatesToQueue:(NSOperationQueue *)queue handler:(void (^)(CMAngle *angle))handler;
19
+ - (void)stopAngleUpdates;
20
+ @end
21
+
22
+ // Prints `angle <degrees>` for the first sample and for every later change of at
23
+ // least 0.5°, until the parent closes stdin. Read-only: it never creates the HID
24
+ // services, so it can run next to the command transport. Reading in the guest
25
+ // keeps the hinge independent of CoreDevice: `devicectl device motion` exists in
26
+ // devicectl 651.13.4 (Xcode 27.1 beta, 27A9269) but not in 651.13.5 (Xcode 27.1
27
+ // 27A9275) or later (seen through 651.32, Xcode 27.2 27B52), and CoreDevice is
28
+ // shared by every installed Xcode.
29
+ static int watchAngle(void) {
30
+ dlopen("/System/Library/Frameworks/CoreMotion.framework/CoreMotion", RTLD_NOW);
31
+ Class managerClass = NSClassFromString(@"CMAngleManager");
32
+ if (!managerClass || ![managerClass respondsToSelector:@selector(isAvailable)] || ![managerClass isAvailable]) {
33
+ fprintf(stderr, "Hinge angle unavailable: CMAngleManager reports no hinge\n");
34
+ return 2;
35
+ }
36
+ CMAngleManager *manager = [managerClass new];
37
+ if (![manager respondsToSelector:@selector(startAngleUpdatesToQueue:handler:)]) {
38
+ fprintf(stderr, "Hinge angle unavailable: CMAngleManager has no angle updates\n");
39
+ return 2;
40
+ }
41
+ NSOperationQueue *queue = [NSOperationQueue new];
42
+ queue.maxConcurrentOperationCount = 1;
43
+ __block double last = NAN;
44
+ [manager startAngleUpdatesToQueue:queue handler:^(CMAngle *sample) {
45
+ double degrees = [sample angleDegrees];
46
+ if (!isfinite(degrees) || degrees < 0 || degrees > 180) return;
47
+ if (isfinite(last) && fabs(degrees - last) < 0.5) return;
48
+ last = degrees;
49
+ if (degrees == 0) degrees = 0; // never print -0.0
50
+ printf("angle %.1f\n", degrees);
51
+ fflush(stdout);
52
+ }];
53
+ // Updates arrive on `queue`, not the main run loop, so the main thread only
54
+ // waits for the parent to close stdin.
55
+ char buffer[64];
56
+ while (read(STDIN_FILENO, buffer, sizeof buffer) > 0) {}
57
+ [manager stopAngleUpdates];
58
+ return 0;
59
+ }
60
+
11
61
  @interface DuoServiceDelegate : NSObject
12
62
  @property NSDictionary *properties;
13
63
  @end
@@ -87,8 +137,10 @@ static BOOL sendEvent(id service, CFTypeRef event) {
87
137
  CFRelease(event);
88
138
  return ok;
89
139
  }
90
- int main(void) {
140
+ int main(int argc, char **argv) {
91
141
  @autoreleasepool {
142
+ if (argc == 2 && strcmp(argv[1], "watch-angle") == 0) return watchAngle();
143
+ if (argc != 1) { fprintf(stderr, "usage: serve-sim-duo-hid [watch-angle]\n"); return 64; }
92
144
  dlopen("/System/Library/PrivateFrameworks/HID.framework/HID", RTLD_NOW);
93
145
  void *io = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", RTLD_NOW);
94
146
  CFTypeRef (*vendor)(CFAllocatorRef, uint64_t, uint32_t, uint32_t, uint32_t, const void *, CFIndex, uint32_t) = dlsym(io, "IOHIDEventCreateVendorDefinedEvent");