From 596083f30f5065ecf131798fbc31417544d665bc Mon Sep 17 00:00:00 2001 From: tastyheadphones Date: Thu, 5 Mar 2026 09:31:06 +0900 Subject: [PATCH 1/2] location: add presets and route replay controls --- research/patch_comparison_all_variants.md | 5 + sources/vphone-cli/VPhoneAppDelegate.swift | 2 + .../vphone-cli/VPhoneLocationProvider.swift | 156 +++++++++++++++++- sources/vphone-cli/VPhoneMenuController.swift | 3 + sources/vphone-cli/VPhoneMenuLocation.swift | 126 ++++++++++++++ 5 files changed, 291 insertions(+), 1 deletion(-) diff --git a/research/patch_comparison_all_variants.md b/research/patch_comparison_all_variants.md index 68b0030..b37b56a 100644 --- a/research/patch_comparison_all_variants.md +++ b/research/patch_comparison_all_variants.md @@ -218,6 +218,11 @@ Why `ramdisk_build` still prints patch logs: - Step 6 patches `Firmware/txm.iphoneos.release.im4p` via `patch_txm()` (1 trustcache-bypass patch), then signs `Ramdisk/txm.img4`. - Step 7 may derive `kernelcache.research.vphone600.ramdisk` from pristine CloudOS and apply base `KernelPatcher` (28 patches), then signs `Ramdisk/krnl.ramdisk.img4`. + +## Host Tooling Note (2026-03-05) + +- Added host-side location simulation UX improvements (`Location` menu presets + route replay controls) in `vphone-cli`. +- This change does **not** alter firmware patch counts or variant composition above; it uses the existing `vphoned` location protocol. - Step 7 also always signs restore kernel as `Ramdisk/krnl.img4`. | Variant | Pre-step before `make ramdisk_build` | `Ramdisk/txm.img4` | `Ramdisk/krnl.ramdisk.img4` | `Ramdisk/krnl.img4` | Effective kernel used by `ramdisk_send.sh` | diff --git a/sources/vphone-cli/VPhoneAppDelegate.swift b/sources/vphone-cli/VPhoneAppDelegate.swift index 816ac80..9060945 100644 --- a/sources/vphone-cli/VPhoneAppDelegate.swift +++ b/sources/vphone-cli/VPhoneAppDelegate.swift @@ -150,6 +150,7 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate { } } control.onDisconnect = { [weak mc, weak provider = locationProvider] in + provider?.stopReplay() provider?.stopForwarding() mc?.updateLocationCapability(available: false) } @@ -163,6 +164,7 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate { } } control.onDisconnect = { [weak provider = locationProvider] in + provider?.stopReplay() provider?.stopForwarding() } } diff --git a/sources/vphone-cli/VPhoneLocationProvider.swift b/sources/vphone-cli/VPhoneLocationProvider.swift index e64b038..e859571 100644 --- a/sources/vphone-cli/VPhoneLocationProvider.swift +++ b/sources/vphone-cli/VPhoneLocationProvider.swift @@ -9,12 +9,44 @@ import Foundation /// after vphoned reconnects) — re-sends the last known position. @MainActor class VPhoneLocationProvider: NSObject { + struct ReplayPoint { + let latitude: Double + let longitude: Double + let altitude: Double + let horizontalAccuracy: Double + let verticalAccuracy: Double + let speed: Double + let course: Double + + init( + latitude: Double, + longitude: Double, + altitude: Double = 0, + horizontalAccuracy: Double = 5, + verticalAccuracy: Double = 8, + speed: Double = 0, + course: Double = -1 + ) { + self.latitude = latitude + self.longitude = longitude + self.altitude = altitude + self.horizontalAccuracy = horizontalAccuracy + self.verticalAccuracy = verticalAccuracy + self.speed = speed + self.course = course + } + } + private let control: VPhoneControl private var hostModeStarted = false private var locationManager: CLLocationManager? private var delegateProxy: LocationDelegateProxy? private var lastLocation: CLLocation? + private var replayTask: Task? + private var replayName: String? + + var isReplaying: Bool { replayTask != nil } init(control: VPhoneControl) { self.control = control @@ -35,6 +67,7 @@ class VPhoneLocationProvider: NSObject { /// Begin sending location to the guest. Safe to call on every (re)connect. func startForwarding() { + stopReplay() guard let mgr = locationManager else { return } mgr.requestAlwaysAuthorization() mgr.startUpdatingLocation() @@ -47,7 +80,7 @@ class VPhoneLocationProvider: NSObject { } } - /// Stop forwarding and clear the simulated location in the guest. + /// Stop forwarding host location updates. func stopForwarding() { if hostModeStarted { locationManager?.stopUpdatingLocation() @@ -56,6 +89,93 @@ class VPhoneLocationProvider: NSObject { } } + /// Send a fixed simulated location to the guest. + func sendPreset(name: String, latitude: Double, longitude: Double, altitude: Double = 0) { + stopReplay() + sendSimulatedLocation( + latitude: latitude, + longitude: longitude, + altitude: altitude, + horizontalAccuracy: 5, + verticalAccuracy: 8, + speed: 0, + course: -1 + ) + print("[location] applied preset '\(name)' (\(latitude), \(longitude))") + } + + /// Start replaying a list of simulated locations at a fixed interval. + func startReplay( + name: String, + points: [ReplayPoint], + intervalSeconds: Double = 1.5, + loop: Bool = true + ) { + guard !points.isEmpty else { + print("[location] replay '\(name)' ignored: no points") + return + } + + stopForwarding() + stopReplay() + + replayName = name + let sleepNanos = UInt64((max(intervalSeconds, 0.1) * 1_000_000_000).rounded()) + print( + "[location] starting replay '\(name)' (\(points.count) points, interval \(String(format: "%.1f", intervalSeconds))s, loop=\(loop))" + ) + + replayTask = Task { @MainActor [weak self] in + guard let self else { return } + defer { + self.replayTask = nil + self.replayName = nil + } + + var index = 0 + while !Task.isCancelled { + let point = points[index] + self.sendSimulatedLocation( + latitude: point.latitude, + longitude: point.longitude, + altitude: point.altitude, + horizontalAccuracy: point.horizontalAccuracy, + verticalAccuracy: point.verticalAccuracy, + speed: point.speed, + course: point.course + ) + + index += 1 + if index >= points.count { + if loop { + index = 0 + } else { + break + } + } + + try? await Task.sleep(nanoseconds: sleepNanos) + } + + if Task.isCancelled { + print("[location] replay cancelled: \(name)") + } else { + print("[location] replay finished: \(name)") + } + } + } + + /// Stop an active replay task. + func stopReplay() { + guard let replayTask else { return } + replayTask.cancel() + self.replayTask = nil + if let replayName { + print("[location] stopped replay: \(replayName)") + } + replayName = nil + } + private func forward(_ location: CLLocation) { lastLocation = location guard control.isConnected else { @@ -72,6 +192,40 @@ class VPhoneLocationProvider: NSObject { course: location.course ) } + + private func sendSimulatedLocation( + latitude: Double, + longitude: Double, + altitude: Double, + horizontalAccuracy: Double, + verticalAccuracy: Double, + speed: Double, + course: Double + ) { + let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + lastLocation = CLLocation( + coordinate: coordinate, + altitude: altitude, + horizontalAccuracy: horizontalAccuracy, + verticalAccuracy: verticalAccuracy, + timestamp: Date() + ) + + guard control.isConnected else { + print("[location] simulate: not connected, cached for later") + return + } + + control.sendLocation( + latitude: latitude, + longitude: longitude, + altitude: altitude, + horizontalAccuracy: horizontalAccuracy, + verticalAccuracy: verticalAccuracy, + speed: speed, + course: course + ) + } } // MARK: - CLLocationManagerDelegate Proxy diff --git a/sources/vphone-cli/VPhoneMenuController.swift b/sources/vphone-cli/VPhoneMenuController.swift index fb56234..6fb4813 100644 --- a/sources/vphone-cli/VPhoneMenuController.swift +++ b/sources/vphone-cli/VPhoneMenuController.swift @@ -12,6 +12,9 @@ class VPhoneMenuController { var onFilesPressed: (() -> Void)? var locationProvider: VPhoneLocationProvider? var locationMenuItem: NSMenuItem? + var locationPresetMenuItem: NSMenuItem? + var locationReplayStartItem: NSMenuItem? + var locationReplayStopItem: NSMenuItem? var screenRecorder: VPhoneScreenRecorder? var recordingItem: NSMenuItem? var signer: VPhoneSigner? diff --git a/sources/vphone-cli/VPhoneMenuLocation.swift b/sources/vphone-cli/VPhoneMenuLocation.swift index 3183602..50379ec 100644 --- a/sources/vphone-cli/VPhoneMenuLocation.swift +++ b/sources/vphone-cli/VPhoneMenuLocation.swift @@ -1,16 +1,90 @@ import AppKit +private struct LocationPreset { + let title: String + let latitude: Double + let longitude: Double + let altitude: Double +} + +private let locationPresets: [LocationPreset] = [ + LocationPreset( + title: "Apple Park (Cupertino)", + latitude: 37.334606, + longitude: -122.009102, + altitude: 14 + ), + LocationPreset( + title: "SF Ferry Building", + latitude: 37.795490, + longitude: -122.393738, + altitude: 5 + ), + LocationPreset( + title: "Times Square (NYC)", + latitude: 40.758000, + longitude: -73.985500, + altitude: 12 + ), + LocationPreset( + title: "Shibuya Crossing (Tokyo)", + latitude: 35.659500, + longitude: 139.700500, + altitude: 38 + ), +] + +private let locationReplayName = "Apple Park Loop" +private let locationReplayPoints: [VPhoneLocationProvider.ReplayPoint] = [ + .init(latitude: 37.334606, longitude: -122.009102, altitude: 14, speed: 6.5, course: 240), + .init(latitude: 37.333660, longitude: -122.011700, altitude: 14, speed: 7.0, course: 255), + .init(latitude: 37.332500, longitude: -122.014200, altitude: 14, speed: 7.2, course: 300), + .init(latitude: 37.333300, longitude: -122.016000, altitude: 14, speed: 6.6, course: 20), + .init(latitude: 37.335100, longitude: -122.016300, altitude: 14, speed: 6.4, course: 55), + .init(latitude: 37.337000, longitude: -122.014100, altitude: 14, speed: 6.8, course: 95), + .init(latitude: 37.337600, longitude: -122.011200, altitude: 14, speed: 6.9, course: 130), + .init(latitude: 37.336500, longitude: -122.008900, altitude: 14, speed: 6.3, course: 175), +] + // MARK: - Location Menu extension VPhoneMenuController { func buildLocationMenu() -> NSMenuItem { let item = NSMenuItem() let menu = NSMenu(title: "Location") + let toggle = makeItem("Sync Host Location", action: #selector(toggleLocationSync)) toggle.state = .off toggle.isEnabled = false locationMenuItem = toggle menu.addItem(toggle) + + menu.addItem(NSMenuItem.separator()) + + let presets = NSMenuItem(title: "Preset Location", action: nil, keyEquivalent: "") + let presetsMenu = NSMenu(title: "Preset Location") + for (index, preset) in locationPresets.enumerated() { + let presetItem = makeItem(preset.title, action: #selector(setLocationPreset(_:))) + presetItem.tag = index + presetsMenu.addItem(presetItem) + } + presets.submenu = presetsMenu + presets.isEnabled = false + locationPresetMenuItem = presets + menu.addItem(presets) + + menu.addItem(NSMenuItem.separator()) + + let replayStart = makeItem("Start Route Replay", action: #selector(startLocationReplay(_:))) + replayStart.isEnabled = false + locationReplayStartItem = replayStart + menu.addItem(replayStart) + + let replayStop = makeItem("Stop Route Replay", action: #selector(stopLocationReplay(_:))) + replayStop.isEnabled = false + locationReplayStopItem = replayStop + menu.addItem(replayStop) + item.submenu = menu return item } @@ -19,6 +93,8 @@ extension VPhoneMenuController { /// Preserves the user's checkmark state across connect/disconnect cycles. func updateLocationCapability(available: Bool) { locationMenuItem?.isEnabled = available + locationPresetMenuItem?.isEnabled = available + refreshLocationReplayState(available: available) } @objc func toggleLocationSync() { @@ -29,9 +105,59 @@ extension VPhoneMenuController { item.state = .off print("[location] sync toggled off by user") } else { + locationProvider?.stopReplay() locationProvider?.startForwarding() item.state = .on print("[location] sync toggled on by user") } + refreshLocationReplayState(available: item.isEnabled) + } + + @objc func setLocationPreset(_ sender: NSMenuItem) { + guard locationMenuItem?.isEnabled == true else { return } + guard sender.tag >= 0, sender.tag < locationPresets.count else { return } + let preset = locationPresets[sender.tag] + disableHostSyncForManualLocation() + locationProvider?.sendPreset( + name: preset.title, + latitude: preset.latitude, + longitude: preset.longitude, + altitude: preset.altitude + ) + refreshLocationReplayState(available: true) + } + + @objc func startLocationReplay(_: NSMenuItem) { + guard locationMenuItem?.isEnabled == true else { return } + disableHostSyncForManualLocation() + locationProvider?.startReplay( + name: locationReplayName, + points: locationReplayPoints, + intervalSeconds: 1.5, + loop: true + ) + refreshLocationReplayState(available: true) + } + + @objc func stopLocationReplay(_: NSMenuItem) { + locationProvider?.stopReplay() + refreshLocationReplayState(available: locationMenuItem?.isEnabled ?? false) + } + + private func disableHostSyncForManualLocation() { + guard let hostSyncItem = locationMenuItem else { return } + if hostSyncItem.state == .on { + locationProvider?.stopForwarding() + hostSyncItem.state = .off + print("[location] host sync disabled for manual simulation") + } + } + + private func refreshLocationReplayState(available: Bool) { + let replaying = locationProvider?.isReplaying ?? false + locationReplayStartItem?.isEnabled = available && !replaying + locationReplayStopItem?.isEnabled = available && replaying + locationReplayStartItem?.state = replaying ? .on : .off + locationReplayStopItem?.state = replaying ? .on : .off } } From fc96e4439d429c907ca1dbf0a6551655670bf26a Mon Sep 17 00:00:00 2001 From: tastyheadphones Date: Thu, 5 Mar 2026 09:34:19 +0900 Subject: [PATCH 2/2] research: drop unrelated patch comparison note from PR --- research/patch_comparison_all_variants.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/research/patch_comparison_all_variants.md b/research/patch_comparison_all_variants.md index b37b56a..68b0030 100644 --- a/research/patch_comparison_all_variants.md +++ b/research/patch_comparison_all_variants.md @@ -218,11 +218,6 @@ Why `ramdisk_build` still prints patch logs: - Step 6 patches `Firmware/txm.iphoneos.release.im4p` via `patch_txm()` (1 trustcache-bypass patch), then signs `Ramdisk/txm.img4`. - Step 7 may derive `kernelcache.research.vphone600.ramdisk` from pristine CloudOS and apply base `KernelPatcher` (28 patches), then signs `Ramdisk/krnl.ramdisk.img4`. - -## Host Tooling Note (2026-03-05) - -- Added host-side location simulation UX improvements (`Location` menu presets + route replay controls) in `vphone-cli`. -- This change does **not** alter firmware patch counts or variant composition above; it uses the existing `vphoned` location protocol. - Step 7 also always signs restore kernel as `Ramdisk/krnl.img4`. | Variant | Pre-step before `make ramdisk_build` | `Ramdisk/txm.img4` | `Ramdisk/krnl.ramdisk.img4` | `Ramdisk/krnl.img4` | Effective kernel used by `ramdisk_send.sh` |