diff --git a/.DS_Store b/.DS_Store
index d98ebb1..f45a06b 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
index 330d167..3514bbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -88,3 +88,5 @@ fastlane/test_output
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
+
+*.DS_Store
diff --git a/ios_app/iwatch Watch App/Assets.xcassets/AccentColor.colorset/Contents.json b/ios_app/iwatch Watch App/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/ios_app/iwatch Watch App/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios_app/iwatch Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios_app/iwatch Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..49c81cd
--- /dev/null
+++ b/ios_app/iwatch Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "watchos",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios_app/iwatch Watch App/Assets.xcassets/Contents.json b/ios_app/iwatch Watch App/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/ios_app/iwatch Watch App/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios_app/iwatch Watch App/ContentView.swift b/ios_app/iwatch Watch App/ContentView.swift
new file mode 100644
index 0000000..2e0c0b0
--- /dev/null
+++ b/ios_app/iwatch Watch App/ContentView.swift
@@ -0,0 +1,114 @@
+//
+// ContentView.swift
+// iwatch Watch App
+//
+// Created by 梁丰洲 on 2024/12/15.
+//
+
+import SwiftUI
+import WatchConnectivity
+
+func quaternionToEulerAngles(w: Double, x: Double, y: Double, z: Double) -> (roll: Double, pitch: Double, yaw: Double) {
+ let sinr_cosp = 2.0 * (w * x + y * z)
+ let cosr_cosp = 1.0 - 2.0 * (x * x + y * y)
+ let roll = atan2(sinr_cosp, cosr_cosp)
+
+ let sinp = 2.0 * (w * y - z * x)
+ let pitch: Double
+ if abs(sinp) >= 1 {
+ pitch = sinp > 0 ? Double.pi / 2 : -Double.pi / 2
+ } else {
+ pitch = asin(sinp)
+ }
+
+ let siny_cosp = 2.0 * (w * z + x * y)
+ let cosy_cosp = 1.0 - 2.0 * (y * y + z * z)
+ let yaw = atan2(siny_cosp, cosy_cosp)
+
+ return (roll, pitch, yaw)
+}
+
+struct ContentView: View {
+ @State private var logStarting = false
+ @State private var showAlert = false
+ @ObservedObject var sensorLogger = WatchSensorManager.shared
+
+ var body: some View {
+ VStack {
+ Text("Watch IMU Recorder")
+ Button(action: {
+ if !WCSession.default.isReachable {
+ self.showAlert = true
+ return
+ }
+
+ self.logStarting.toggle()
+
+ if self.logStarting {
+ var samplingFrequency = UserDefaults.standard.integer(forKey: "frequency_preference")
+
+ if samplingFrequency <= 0 {
+ samplingFrequency = 30
+ }
+
+ print("sampling frequency = \(samplingFrequency) on watch")
+ self.sensorLogger.startUpdate(Double(samplingFrequency))
+ }
+ else {
+ self.sensorLogger.stopUpdate()
+ }
+ }) {
+ if self.logStarting {
+ Image(systemName: "pause.circle")
+ Text("stop pushing")
+ }
+ else {
+ Image(systemName: "play.circle")
+ Text("Push data")
+ }
+ }.alert(isPresented: $showAlert) {
+ Alert(title: Text("Watch Not Active"), message: Text("Please activate your watch by raising your wrist before pushing data."), dismissButton: .default(Text("OK")))
+ }
+ }
+
+ VStack {
+ VStack {
+ Text("Accelerometer").font(.headline)
+ HStack {
+ Text(String(format: "%.2f", self.sensorLogger.accX))
+ Spacer()
+ Text(String(format: "%.2f", self.sensorLogger.accY))
+ Spacer()
+ Text(String(format: "%.2f", self.sensorLogger.accZ))
+ }.padding(.horizontal)
+ }
+
+ VStack {
+ Text("Gyroscope").font(.headline)
+ HStack {
+ Text(String(format: "%.2f", self.sensorLogger.gyrX))
+ Spacer()
+ Text(String(format: "%.2f", self.sensorLogger.gyrX))
+ Spacer()
+ Text(String(format: "%.2f", self.sensorLogger.gyrX))
+ }.padding(.horizontal)
+ }
+
+ VStack {
+ HStack {
+ let (roll, pitch, yaw) = quaternionToEulerAngles(w: self.sensorLogger.qatW, x: self.sensorLogger.qatX, y: self.sensorLogger.qatY, z: self.sensorLogger.qatZ)
+
+ Text(String(format: "%.2f", roll * 180.0 / Double.pi))
+ Spacer()
+ Text(String(format: "%.2f", pitch * 180.0 / Double.pi))
+ Spacer()
+ Text(String(format: "%.2f", yaw * 180.0 / Double.pi))
+ }.padding(.horizontal)
+ }
+ }
+ }
+}
+
+#Preview {
+ ContentView()
+}
diff --git a/ios_app/iwatch Watch App/ExtendedRuntimeManager.swift b/ios_app/iwatch Watch App/ExtendedRuntimeManager.swift
new file mode 100644
index 0000000..f612e70
--- /dev/null
+++ b/ios_app/iwatch Watch App/ExtendedRuntimeManager.swift
@@ -0,0 +1,53 @@
+//
+// ExtendedRuntimeManager.swift
+// phone
+//
+// Created by 梁丰洲 on 2024/12/16.
+//
+
+import WatchKit
+
+class ExtendedRuntimeManager: NSObject, WKExtendedRuntimeSessionDelegate {
+ static let shared = ExtendedRuntimeManager()
+
+ private var session: WKExtendedRuntimeSession?
+
+ private override init() {
+ super.init()
+ }
+
+ func startSession() {
+ guard session == nil else { return }
+
+ session = WKExtendedRuntimeSession()
+ session?.delegate = self
+ session?.start()
+
+ print("Extended Runtime Session started.")
+ }
+
+ func endSession() {
+ session?.invalidate()
+ session = nil
+
+ print("Extended Runtime Session ended.")
+ }
+
+ // MARK: - WKExtendedRuntimeSessionDelegate
+
+ func extendedRuntimeSessionDidStart(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
+ print("Extended Runtime Session did start.")
+ }
+
+ func extendedRuntimeSessionWillExpire(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
+ print("Extended Runtime Session will expire.")
+ }
+
+ func extendedRuntimeSession(_ extendedRuntimeSession: WKExtendedRuntimeSession, didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error?) {
+ print("Extended Runtime Session invalidated: \(reason.rawValue), error: \(String(describing: error))")
+ session = nil
+ if reason == .expired {
+ startSession()
+ }
+ }
+}
diff --git a/ios_app/iwatch Watch App/PhoneConnector.swift b/ios_app/iwatch Watch App/PhoneConnector.swift
new file mode 100644
index 0000000..3233feb
--- /dev/null
+++ b/ios_app/iwatch Watch App/PhoneConnector.swift
@@ -0,0 +1,40 @@
+//
+// PhoneConnector.swift
+// phone
+//
+// Created by 梁丰洲 on 2024/12/15.
+//
+
+import WatchConnectivity
+import WatchKit
+
+class PhoneConnector: NSObject, WCSessionDelegate {
+
+ override init() {
+ super.init()
+ if WCSession.isSupported() {
+ WCSession.default.delegate = self
+ WCSession.default.activate()
+ }
+ }
+
+ func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: (any Error)?) {
+ print("watch activationDidCompleteWith state = \(activationState.rawValue)")
+ }
+
+ func sessionReachabilityDidChange(_ session: WCSession) {
+ // 当iPhone或Watch端连通性改变时会调用
+ print("watch sessionReachabilityDidChange")
+ }
+
+ func sendPhoneMessage(data: [String: Any]) {
+ let session = WCSession.default
+ if session.isReachable {
+ session.sendMessage(data, replyHandler: nil, errorHandler: { (error) in
+ print("watch send message to iPhone error: \(error.localizedDescription)")
+ })
+ } else {
+ print("watch session is not reachable")
+ }
+ }
+}
diff --git a/ios_app/iwatch Watch App/Preview Content/Preview Assets.xcassets/Contents.json b/ios_app/iwatch Watch App/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/ios_app/iwatch Watch App/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios_app/iwatch Watch App/WatchSensorManager.swift b/ios_app/iwatch Watch App/WatchSensorManager.swift
new file mode 100644
index 0000000..ab692c2
--- /dev/null
+++ b/ios_app/iwatch Watch App/WatchSensorManager.swift
@@ -0,0 +1,149 @@
+//
+// WatchSensorManager.swift
+// phone
+//
+// Created by 梁丰洲 on 2024/12/15.
+//
+
+import Foundation
+import CoreMotion
+import Combine
+import WatchKit
+
+
+struct WatchSensorData: Codable {
+ let timestamp: TimeInterval
+ let accX: Double
+ let accY: Double
+ let accZ: Double
+ let gyrX: Double
+ let gyrY: Double
+ let gyrZ: Double
+ let qatX: Double
+ let qatY: Double
+ let qatZ: Double
+ let qatW: Double
+}
+
+class WatchSensorManager: NSObject, ObservableObject {
+ static let shared = WatchSensorManager()
+ let motionManager: CMMotionManager
+ let phoneConnector: PhoneConnector
+ let encoder: PropertyListEncoder
+
+ // runtime
+ var timer: Timer?
+
+ @Published var accX = 0.0
+ @Published var accY = 0.0
+ @Published var accZ = 0.0
+ @Published var gyrX = 0.0
+ @Published var gyrY = 0.0
+ @Published var gyrZ = 0.0
+ @Published var qatX = 0.0
+ @Published var qatY = 0.0
+ @Published var qatZ = 0.0
+ @Published var qatW = 0.0
+
+ private override init() {
+ self.motionManager = CMMotionManager()
+ self.phoneConnector = PhoneConnector()
+ self.encoder = PropertyListEncoder()
+ self.encoder.outputFormat = .binary
+ super.init()
+ }
+
+ func sendWatchSensorData() {
+ let sensorData = WatchSensorData(
+ timestamp: Date().timeIntervalSince1970,
+ accX: self.accX,
+ accY: self.accY,
+ accZ: self.accZ,
+ gyrX: self.gyrX,
+ gyrY: self.gyrY,
+ gyrZ: self.gyrZ,
+ qatX: self.qatX,
+ qatY: self.qatY,
+ qatZ: self.qatZ,
+ qatW: self.qatW
+ )
+
+ do {
+ let encodedData = try encoder.encode(sensorData)
+ phoneConnector.sendPhoneMessage(data: ["WATCH_SENSOR_DATA": encodedData])
+ } catch {
+ print("Failed to encode sensor data: \(error)")
+ }
+ }
+
+ func startUpdate(_ freq: Double) {
+ let interval = 1.0 / freq
+
+ // MotionManager
+ if motionManager.isDeviceMotionAvailable {
+ motionManager.deviceMotionUpdateInterval = interval
+ motionManager.startDeviceMotionUpdates()
+ }
+
+ // Timer
+ self.timer = Timer.scheduledTimer(timeInterval: interval,
+ target: self,
+ selector: #selector(self.onLogSensor),
+ userInfo: nil,
+ repeats: true)
+
+ // ExtendedRuntime
+ ExtendedRuntimeManager.shared.startSession()
+
+ // HealthKit
+ WorkoutManager.shared.startWorkout()
+ }
+
+ func stopUpdate() {
+ // ExtendedRuntime
+ ExtendedRuntimeManager.shared.endSession()
+
+ // HealthKit
+ WorkoutManager.shared.endWorkout()
+
+ // Timer
+ self.timer?.invalidate()
+
+ // MotionManager
+ if motionManager.isDeviceMotionActive {
+ motionManager.stopDeviceMotionUpdates()
+ }
+ }
+
+ @objc private func onLogSensor() {
+ if let data = motionManager.deviceMotion {
+ self.accX = data.userAcceleration.x
+ self.accY = data.userAcceleration.y
+ self.accZ = data.userAcceleration.z
+
+ self.gyrX = data.rotationRate.x
+ self.gyrY = data.rotationRate.y
+ self.gyrZ = data.rotationRate.z
+
+ self.qatX = data.attitude.quaternion.x
+ self.qatY = data.attitude.quaternion.y
+ self.qatZ = data.attitude.quaternion.z
+ self.qatW = data.attitude.quaternion.w
+
+ sendWatchSensorData()
+ } else {
+ self.accX = Double.nan
+ self.accY = Double.nan
+ self.accZ = Double.nan
+ self.gyrX = Double.nan
+ self.gyrY = Double.nan
+ self.gyrZ = Double.nan
+ self.qatX = Double.nan
+ self.qatY = Double.nan
+ self.qatZ = Double.nan
+ self.qatW = Double.nan
+ }
+
+ }
+
+}
diff --git a/ios_app/iwatch Watch App/WorkoutManager.swift b/ios_app/iwatch Watch App/WorkoutManager.swift
new file mode 100644
index 0000000..e5fdc51
--- /dev/null
+++ b/ios_app/iwatch Watch App/WorkoutManager.swift
@@ -0,0 +1,66 @@
+//
+// WokeoutManager.swift
+// phone
+//
+// Created by 梁丰洲 on 2024/12/16.
+//
+
+import HealthKit
+
+class WorkoutManager: NSObject, HKWorkoutSessionDelegate {
+ static let shared = WorkoutManager()
+ private var workoutSession: HKWorkoutSession?
+
+ private override init() {
+ super.init()
+ }
+
+ func startWorkout() {
+ let configuration = HKWorkoutConfiguration()
+ configuration.activityType = .other // 根据实际情况选择,如 .running、.cycling 等
+ configuration.locationType = .indoor // 根据实际情况选择
+
+ let healthStore = HKHealthStore()
+
+ do {
+ workoutSession = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
+ workoutSession?.delegate = self
+ workoutSession?.startActivity(with: nil)
+ print("Workout Session START!")
+ } catch {
+ print("Cannot launch Workout Session: \(error.localizedDescription)")
+ }
+ }
+
+ func endWorkout() {
+ if let session = workoutSession {
+ session.end()
+ print("Workout Session END!")
+ }
+ }
+
+ // MARK: - HKWorkoutSessionDelegate
+
+ func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) {
+ switch toState {
+ case .running:
+ startDataCollection()
+ case .ended:
+ stopDataCollection()
+ default:
+ break
+ }
+ }
+
+ func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
+ print("Workout Session 失败: \(error.localizedDescription)")
+ }
+
+ // MARK: - 数据采集
+ func startDataCollection() {
+ }
+
+ func stopDataCollection() {
+ }
+
+}
diff --git a/ios_app/iwatch Watch App/iwatch Watch App.entitlements b/ios_app/iwatch Watch App/iwatch Watch App.entitlements
new file mode 100644
index 0000000..54bc426
--- /dev/null
+++ b/ios_app/iwatch Watch App/iwatch Watch App.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.developer.healthkit
+
+ com.apple.developer.healthkit.background-delivery
+
+
+
diff --git a/ios_app/iwatch Watch App/iwatchApp.swift b/ios_app/iwatch Watch App/iwatchApp.swift
new file mode 100644
index 0000000..60334dc
--- /dev/null
+++ b/ios_app/iwatch Watch App/iwatchApp.swift
@@ -0,0 +1,17 @@
+//
+// iwatchApp.swift
+// iwatch Watch App
+//
+// Created by 梁丰洲 on 2024/12/15.
+//
+
+import SwiftUI
+
+@main
+struct iwatch_Watch_AppApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
diff --git a/ios_app/iwatch-Watch-App-Info.plist b/ios_app/iwatch-Watch-App-Info.plist
new file mode 100644
index 0000000..81c94e4
--- /dev/null
+++ b/ios_app/iwatch-Watch-App-Info.plist
@@ -0,0 +1,15 @@
+
+
+
+
+ UIBackgroundModes
+
+ location
+
+ WKBackgroundModes
+
+ self-care
+ workout-processing
+
+
+
diff --git a/ios_app/phone.xcodeproj/project.pbxproj b/ios_app/phone.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..0db25d8
--- /dev/null
+++ b/ios_app/phone.xcodeproj/project.pbxproj
@@ -0,0 +1,505 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 77;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ F023A8972D0E966C00849073 /* iwatch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = F023A88A2D0E966A00849073 /* iwatch Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ F023A8952D0E966C00849073 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = F023A86D2D0E95CF00849073 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = F023A8892D0E966A00849073;
+ remoteInfo = "iwatch Watch App";
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ F023A89B2D0E966C00849073 /* Embed Watch Content */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
+ dstSubfolderSpec = 16;
+ files = (
+ F023A8972D0E966C00849073 /* iwatch Watch App.app in Embed Watch Content */,
+ );
+ name = "Embed Watch Content";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ F023A8752D0E95CF00849073 /* phone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = phone.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ F023A88A2D0E966A00849073 /* iwatch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iwatch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ F023A8772D0E95CF00849073 /* phone */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = phone;
+ sourceTree = "";
+ };
+ F023A88B2D0E966A00849073 /* iwatch Watch App */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = "iwatch Watch App";
+ sourceTree = "";
+ };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ F023A8722D0E95CF00849073 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ F023A8872D0E966A00849073 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ F023A86C2D0E95CF00849073 = {
+ isa = PBXGroup;
+ children = (
+ F023A8772D0E95CF00849073 /* phone */,
+ F023A88B2D0E966A00849073 /* iwatch Watch App */,
+ F023A8762D0E95CF00849073 /* Products */,
+ );
+ sourceTree = "";
+ };
+ F023A8762D0E95CF00849073 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ F023A8752D0E95CF00849073 /* phone.app */,
+ F023A88A2D0E966A00849073 /* iwatch Watch App.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ F023A8742D0E95CF00849073 /* phone */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = F023A8832D0E95D100849073 /* Build configuration list for PBXNativeTarget "phone" */;
+ buildPhases = (
+ F023A8712D0E95CF00849073 /* Sources */,
+ F023A8722D0E95CF00849073 /* Frameworks */,
+ F023A8732D0E95CF00849073 /* Resources */,
+ F023A89B2D0E966C00849073 /* Embed Watch Content */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ F023A8962D0E966C00849073 /* PBXTargetDependency */,
+ );
+ fileSystemSynchronizedGroups = (
+ F023A8772D0E95CF00849073 /* phone */,
+ );
+ name = phone;
+ packageProductDependencies = (
+ );
+ productName = phone;
+ productReference = F023A8752D0E95CF00849073 /* phone.app */;
+ productType = "com.apple.product-type.application";
+ };
+ F023A8892D0E966A00849073 /* iwatch Watch App */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = F023A8982D0E966C00849073 /* Build configuration list for PBXNativeTarget "iwatch Watch App" */;
+ buildPhases = (
+ F023A8862D0E966A00849073 /* Sources */,
+ F023A8872D0E966A00849073 /* Frameworks */,
+ F023A8882D0E966A00849073 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ F023A88B2D0E966A00849073 /* iwatch Watch App */,
+ );
+ name = "iwatch Watch App";
+ packageProductDependencies = (
+ );
+ productName = "iwatch Watch App";
+ productReference = F023A88A2D0E966A00849073 /* iwatch Watch App.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ F023A86D2D0E95CF00849073 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1620;
+ LastUpgradeCheck = 1620;
+ TargetAttributes = {
+ F023A8742D0E95CF00849073 = {
+ CreatedOnToolsVersion = 16.2;
+ };
+ F023A8892D0E966A00849073 = {
+ CreatedOnToolsVersion = 16.2;
+ };
+ };
+ };
+ buildConfigurationList = F023A8702D0E95CF00849073 /* Build configuration list for PBXProject "phone" */;
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = F023A86C2D0E95CF00849073;
+ minimizedProjectReferenceProxies = 1;
+ preferredProjectObjectVersion = 77;
+ productRefGroup = F023A8762D0E95CF00849073 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ F023A8742D0E95CF00849073 /* phone */,
+ F023A8892D0E966A00849073 /* iwatch Watch App */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ F023A8732D0E95CF00849073 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ F023A8882D0E966A00849073 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ F023A8712D0E95CF00849073 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ F023A8862D0E966A00849073 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ F023A8962D0E966C00849073 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = F023A8892D0E966A00849073 /* iwatch Watch App */;
+ targetProxy = F023A8952D0E966C00849073 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ F023A8812D0E95D100849073 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.2;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ F023A8822D0E95D100849073 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.2;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ F023A8842D0E95D100849073 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"phone/Preview Content\"";
+ DEVELOPMENT_TEAM = ZR7SZVNKWV;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSMotionUsageDescription = "usd to collect IMU data for motion detection";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.6;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "sybil.imu-proj.phone";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ F023A8852D0E95D100849073 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"phone/Preview Content\"";
+ DEVELOPMENT_TEAM = ZR7SZVNKWV;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSMotionUsageDescription = "usd to collect IMU data for motion detection";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.6;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "sybil.imu-proj.phone";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ F023A8992D0E966C00849073 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = "iwatch Watch App/iwatch Watch App.entitlements";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"iwatch Watch App/Preview Content\"";
+ DEVELOPMENT_TEAM = ZR7SZVNKWV;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "iwatch-Watch-App-Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = iwatch;
+ INFOPLIST_KEY_NSHealthClinicalHealthRecordsShareUsageDescription = "We need to update your health data in order to record and analyze your exercise data.";
+ INFOPLIST_KEY_NSHealthShareUsageDescription = "We need access to your health data in order to record your movement data.";
+ INFOPLIST_KEY_NSHealthUpdateUsageDescription = "We need to update your health data in order to record and analyze your exercise data.";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "sybil.imu-proj.phone";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "sybil.imu-proj.phone.watchkitapp";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = watchos;
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 4;
+ WATCHOS_DEPLOYMENT_TARGET = 10.4;
+ };
+ name = Debug;
+ };
+ F023A89A2D0E966C00849073 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = "iwatch Watch App/iwatch Watch App.entitlements";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"iwatch Watch App/Preview Content\"";
+ DEVELOPMENT_TEAM = ZR7SZVNKWV;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "iwatch-Watch-App-Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = iwatch;
+ INFOPLIST_KEY_NSHealthClinicalHealthRecordsShareUsageDescription = "We need to update your health data in order to record and analyze your exercise data.";
+ INFOPLIST_KEY_NSHealthShareUsageDescription = "We need access to your health data in order to record your movement data.";
+ INFOPLIST_KEY_NSHealthUpdateUsageDescription = "We need to update your health data in order to record and analyze your exercise data.";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "sybil.imu-proj.phone";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "sybil.imu-proj.phone.watchkitapp";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = watchos;
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 4;
+ WATCHOS_DEPLOYMENT_TARGET = 10.4;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ F023A8702D0E95CF00849073 /* Build configuration list for PBXProject "phone" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ F023A8812D0E95D100849073 /* Debug */,
+ F023A8822D0E95D100849073 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ F023A8832D0E95D100849073 /* Build configuration list for PBXNativeTarget "phone" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ F023A8842D0E95D100849073 /* Debug */,
+ F023A8852D0E95D100849073 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ F023A8982D0E966C00849073 /* Build configuration list for PBXNativeTarget "iwatch Watch App" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ F023A8992D0E966C00849073 /* Debug */,
+ F023A89A2D0E966C00849073 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = F023A86D2D0E95CF00849073 /* Project object */;
+}
diff --git a/ios_app/phone.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios_app/phone.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/ios_app/phone.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ios_app/phone/Assets.xcassets/AccentColor.colorset/Contents.json b/ios_app/phone/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/ios_app/phone/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios_app/phone/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios_app/phone/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..2305880
--- /dev/null
+++ b/ios_app/phone/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,35 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios_app/phone/Assets.xcassets/Contents.json b/ios_app/phone/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/ios_app/phone/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios_app/phone/ContentView.swift b/ios_app/phone/ContentView.swift
new file mode 100644
index 0000000..a63dd6d
--- /dev/null
+++ b/ios_app/phone/ContentView.swift
@@ -0,0 +1,233 @@
+//
+// ContentView.swift
+// phone
+//
+// Created by 梁丰洲 on 2024/12/15.
+//
+
+import SwiftUI
+
+struct ContentView: View {
+ @ObservedObject private var watchConnector = WatchConnector()
+ @ObservedObject private var sensorLogger = PhoneSensorManager()
+ @ObservedObject private var clientSocket = SocketClient()
+
+ @State private var host = "100.64.1.26"
+ @State private var port = "12345"
+ @State private var logStarting = false
+ @State private var backgroundTaskID: UIBackgroundTaskIdentifier?
+
+ func logAction(_ LargeFeedback: Bool) {
+ self.logStarting.toggle()
+
+ let switchFeedback: UIImpactFeedbackGenerator
+ if LargeFeedback {
+ switchFeedback = UIImpactFeedbackGenerator(style: .heavy)
+ }
+ else {
+ switchFeedback = UIImpactFeedbackGenerator(style: .medium)
+ }
+ switchFeedback.impactOccurred()
+
+ if self.logStarting {
+ // background task
+ self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
+
+ // start to measure
+ var samplingFrequency = UserDefaults.standard.integer(forKey: "frequency_preference")
+ if samplingFrequency <= 0 {
+ samplingFrequency = 30
+ }
+
+ print("sampling frequency = \(samplingFrequency)")
+
+ self.sensorLogger.startUpdate(Double(samplingFrequency))
+ }
+ else {
+ self.sensorLogger.stopUpdate()
+ if let backgroundTaskID = self.backgroundTaskID {
+ UIApplication.shared.endBackgroundTask(backgroundTaskID)
+ }
+ }
+ }
+
+ var body: some View {
+ VStack {
+ VStack{
+ HStack{
+ Spacer()
+ Text("IP")
+ TextField("server ip", text: $host)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .padding(.vertical, 4)
+ .disabled(clientSocket.isConnected || clientSocket.isConnecting)
+ Spacer()
+ Text("Port")
+ TextField("server port", text: $port)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .padding(.vertical, 4)
+ .keyboardType(.numberPad)
+ .disabled(clientSocket.isConnected || clientSocket.isConnecting)
+ Spacer()
+ }.padding([.horizontal])
+ }
+ HStack {
+ Spacer ()
+ // socket state
+ if clientSocket.isConnected {
+ HStack {
+ Image(systemName: "checkmark.circle")
+ Text("online")
+ }.foregroundColor(.green)
+ } else {
+ if clientSocket.isConnecting {
+ HStack {
+ Image(systemName: "arrow.2.circlepath")
+ Text("connecting")
+ }.foregroundColor(.yellow)
+ } else {
+ HStack {
+ Image(systemName: "xmark.circle")
+ Text("offline")
+ }.foregroundColor(.red)
+ }
+ }
+ Spacer ()
+ // server button
+ if clientSocket.isConnected || clientSocket.isConnecting {
+ Button(action: {
+ clientSocket.disconnect()
+ }) {
+ HStack {
+ Image(systemName: "server.rack")
+ Text("disconnect")
+ }
+ }
+ } else {
+ Button(action: {
+ guard let portNum = UInt16(port) else { return }
+ clientSocket.connect(host: host, port: portNum)
+ }) {
+ HStack {
+ Image(systemName: "server.rack")
+ Text("conect")
+ }
+ }
+ }
+ Spacer ()
+ // start or stop
+ Button(action: {}) {
+ if self.logStarting {
+ // double click or long touch
+ HStack {
+ Image(systemName: "pause.circle")
+ Text("Stop")
+ }
+ .onTapGesture(count: 2, perform: {
+ self.logAction(true)
+ })
+ .onLongPressGesture {
+ self.logAction(true)
+ }
+ }
+ else {
+ HStack {
+ Image(systemName: "play.circle")
+ Text("Start")
+ }.onTapGesture {
+ self.logAction(false)
+ }
+
+ }
+ }
+ Spacer ()
+ }
+ .padding(.vertical)
+
+ // 根据设备类型(iPhone/iPad)显示对应数据
+ if UIDevice.current.userInterfaceIdiom == .phone {
+ deviceView(
+ deviceName: "iPhone",
+ deviceIcon: "iphone",
+ sensorData: sensorLogger.phoneSensorData
+ )
+ } else if UIDevice.current.userInterfaceIdiom == .pad {
+ deviceView(
+ deviceName: "iPad",
+ deviceIcon: "ipad",
+ sensorData: sensorLogger.phoneSensorData
+ )
+ }
+
+ // Watch数据(非iPad时显示)
+ if UIDevice.current.userInterfaceIdiom != .pad {
+ deviceView(
+ deviceName: "Watch",
+ deviceIcon: "applewatch",
+ sensorData: watchConnector.sensorData?.toSensorData()
+ )
+ }
+
+ // AirPods数据
+ deviceView(
+ deviceName: "AirPods Pro",
+ deviceIcon: "airpodspro",
+ sensorData: sensorLogger.headSensorData
+ )
+
+ }
+ }
+
+
+ func deviceView(deviceName: String, deviceIcon: String, sensorData: SensorData?) -> some View {
+ VStack {
+ HStack {
+ Image(systemName: deviceIcon)
+ Text(deviceName).font(.headline)
+ }
+
+ HStack {
+ Image(systemName: "speedometer")
+ Spacer()
+ Text(String(format: "%.3f", sensorData?.accX ?? Double.nan))
+ Spacer()
+ Text(String(format: "%.3f", sensorData?.accY ?? Double.nan))
+ Spacer()
+ Text(String(format: "%.3f", sensorData?.accZ ?? Double.nan))
+ }
+ .padding(.horizontal, 30)
+ .padding(.vertical, 2)
+
+ HStack {
+ Image(systemName: "gyroscope")
+ Spacer()
+ Text(String(format: "%.3f", sensorData?.gyrX ?? Double.nan))
+ Spacer()
+ Text(String(format: "%.3f", sensorData?.gyrY ?? Double.nan))
+ Spacer()
+ Text(String(format: "%.3f", sensorData?.gyrZ ?? Double.nan))
+ }
+ .padding(.horizontal, 30)
+ .padding(.vertical, 2)
+
+ HStack {
+ let (roll, pitch, yaw) = sensorData?.quaternionToEulerAngles() ?? (Double.nan, Double.nan, Double.nan)
+ Image(systemName: "rotate.3d")
+ Spacer()
+ Text(String(format: "%.3f", roll * 180.0 / Double.pi))
+ Spacer()
+ Text(String(format: "%.3f", pitch * 180.0 / Double.pi))
+ Spacer()
+ Text(String(format: "%.3f", yaw * 180.0 / Double.pi))
+ }
+ .padding(.horizontal, 30)
+ .padding(.vertical, 2)
+ }
+ .padding(.vertical, 10)
+ }
+
+}
+
+#Preview {
+ ContentView()
+}
diff --git a/ios_app/phone/PhoneSensorManager.swift b/ios_app/phone/PhoneSensorManager.swift
new file mode 100644
index 0000000..8b6c590
--- /dev/null
+++ b/ios_app/phone/PhoneSensorManager.swift
@@ -0,0 +1,104 @@
+//
+// PhoneSensorManager.swift
+// phone
+//
+// Created by 梁丰洲 on 2024/12/15.
+//
+
+import Foundation
+import CoreMotion
+import Combine
+
+class PhoneSensorManager: NSObject, ObservableObject {
+ let motionManager: CMMotionManager
+ let headphoneMotionManager: CMHeadphoneMotionManager
+
+ // runtime
+ var timer: Timer?
+
+ @Published var phoneSensorData: SensorData?
+ @Published var headSensorData: SensorData?
+
+ override init() {
+ self.motionManager = CMMotionManager()
+ self.headphoneMotionManager = CMHeadphoneMotionManager()
+ super.init()
+
+ }
+
+ func startUpdate(_ freq: Double) {
+ let interval = 1.0 / freq
+
+ if self.motionManager.isDeviceMotionAvailable {
+ self.motionManager.deviceMotionUpdateInterval = interval
+ self.motionManager.startDeviceMotionUpdates()
+ }
+
+ // head phone has a fixed frequency of 10hz?
+ if self.headphoneMotionManager.isDeviceMotionAvailable {
+ self.headphoneMotionManager.startDeviceMotionUpdates()
+ }
+
+ self.timer = Timer.scheduledTimer(timeInterval: interval,
+ target: self,
+ selector: #selector(self.onLogSensor),
+ userInfo: nil,
+ repeats: true)
+ }
+
+ func stopUpdate() {
+ self.timer?.invalidate()
+
+ if self.headphoneMotionManager.isDeviceMotionActive {
+ self.headphoneMotionManager.stopDeviceMotionUpdates()
+ }
+
+
+ if self.motionManager.isDeviceMotionAvailable {
+ self.motionManager.stopDeviceMotionUpdates()
+ }
+
+ }
+
+ @objc private func onLogSensor() {
+ let timestamp = Date().timeIntervalSince1970
+
+ // iPhone
+ if let motion = motionManager.deviceMotion {
+ let data = SensorData(accX: motion.userAcceleration.x,
+ accY: motion.userAcceleration.y,
+ accZ: motion.userAcceleration.z,
+ gyrX: motion.rotationRate.x,
+ gyrY: motion.rotationRate.y,
+ gyrZ: motion.rotationRate.z,
+ qatX: motion.attitude.quaternion.x,
+ qatY: motion.attitude.quaternion.y,
+ qatZ: motion.attitude.quaternion.z,
+ qatW: motion.attitude.quaternion.w)
+ self.phoneSensorData = data
+ SensorDataManager.shared.append(sensorType: .PHONE, data: data, timestamp: timestamp)
+ } else {
+ self.phoneSensorData = nil
+ }
+
+ // AirPods Pro
+ if let data = self.headphoneMotionManager.deviceMotion {
+ let data = SensorData(accX: data.userAcceleration.x,
+ accY: data.userAcceleration.y,
+ accZ: data.userAcceleration.z,
+ gyrX: data.rotationRate.x,
+ gyrY: data.rotationRate.y,
+ gyrZ: data.rotationRate.z,
+ qatX: data.attitude.quaternion.x,
+ qatY: data.attitude.quaternion.y,
+ qatZ: data.attitude.quaternion.z,
+ qatW: data.attitude.quaternion.w)
+ self.headSensorData = data
+ SensorDataManager.shared.append(sensorType: .HEADSET, data: data, timestamp: timestamp)
+ } else {
+ self.headSensorData = nil
+ }
+
+ }
+
+}
diff --git a/ios_app/phone/Preview Content/Preview Assets.xcassets/Contents.json b/ios_app/phone/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/ios_app/phone/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios_app/phone/SensorData.swift b/ios_app/phone/SensorData.swift
new file mode 100644
index 0000000..bd2607f
--- /dev/null
+++ b/ios_app/phone/SensorData.swift
@@ -0,0 +1,62 @@
+//
+// SensorData.swift
+// phone
+//
+// Created by 梁丰洲 on 2024/12/15.
+//
+
+import SwiftUI
+import Foundation
+
+struct SensorDataRecord: Codable {
+ let timestamp: TimeInterval
+ let sensorType: SensorType
+ let data: SensorData
+}
+
+enum SensorType: String, Codable {
+ case PHONE
+ case WATCH
+ case HEADSET
+}
+
+struct SensorData: Codable {
+ var accX: Double
+ var accY: Double
+ var accZ: Double
+ var gyrX: Double
+ var gyrY: Double
+ var gyrZ: Double
+ var qatX: Double
+ var qatY: Double
+ var qatZ: Double
+ var qatW: Double
+
+ func quaternionToEulerAngles() -> (roll: Double, pitch: Double, yaw: Double) {
+ let w = self.qatW
+ let x = self.qatX
+ let y = self.qatY
+ let z = self.qatZ
+ let sinr_cosp = 2.0 * (w * x + y * z)
+ let cosr_cosp = 1.0 - 2.0 * (x * x + y * y)
+ let roll = atan2(sinr_cosp, cosr_cosp)
+
+ let sinp = 2.0 * (w * y - z * x)
+ let pitch: Double
+ if abs(sinp) >= 1 {
+ pitch = sinp > 0 ? Double.pi / 2 : -Double.pi / 2
+ } else {
+ pitch = asin(sinp)
+ }
+
+ let siny_cosp = 2.0 * (w * z + x * y)
+ let cosy_cosp = 1.0 - 2.0 * (y * y + z * z)
+ let yaw = atan2(siny_cosp, cosy_cosp)
+
+ return (roll, pitch, yaw)
+ }
+
+ func toIMUString() -> String {
+ return "\(self.accX),\(self.accY),\(self.accZ),\(self.qatX),\(self.qatY),\(self.qatZ),\(self.qatW)"
+ }
+}
diff --git a/ios_app/phone/SensorDataManager.swift b/ios_app/phone/SensorDataManager.swift
new file mode 100644
index 0000000..b2ea798
--- /dev/null
+++ b/ios_app/phone/SensorDataManager.swift
@@ -0,0 +1,51 @@
+//
+// SensorDataManager.swift
+// phone
+//
+// Created by 梁丰洲 on 2024/12/15.
+//
+
+import Foundation
+
+class SensorDataManager: @unchecked Sendable {
+ static let shared = SensorDataManager()
+
+ private var records: [SensorDataRecord] = []
+
+ var latestPhone: SensorDataRecord?
+ var latestWatch: SensorDataRecord?
+ var latestHeadset: SensorDataRecord?
+
+ private func appendInternal(record: SensorDataRecord) {
+// self.records.append(record)
+ switch record.sensorType {
+ case .PHONE:
+ if latestPhone == nil || record.timestamp > latestPhone!.timestamp {
+ latestPhone = record
+ }
+ case .WATCH:
+ if latestWatch == nil || record.timestamp > latestWatch!.timestamp {
+ latestWatch = record
+ }
+ case .HEADSET:
+ if latestHeadset == nil || record.timestamp > latestHeadset!.timestamp {
+ latestHeadset = record
+ }
+ }
+ }
+
+ func append(sensorType: SensorType, data: SensorData, timestamp: TimeInterval = Date().timeIntervalSince1970) {
+ let record = SensorDataRecord(timestamp: timestamp, sensorType: sensorType, data: data)
+ self.appendInternal(record: record)
+ }
+
+ func allRecords() -> [SensorDataRecord] {
+ return records
+ }
+
+ func clear() {
+ records = []
+ }
+
+ private init() {}
+}
diff --git a/ios_app/phone/SocketClient.swift b/ios_app/phone/SocketClient.swift
new file mode 100644
index 0000000..8635f7d
--- /dev/null
+++ b/ios_app/phone/SocketClient.swift
@@ -0,0 +1,115 @@
+//
+// SocketClient.swift
+// phone
+//
+// Created by 梁丰洲 on 2024/12/15.
+//
+
+import SwiftUI
+import Network
+
+class SocketClient: ObservableObject {
+ @Published var isConnected: Bool = false
+ @Published var isConnecting: Bool = false
+
+ private var connection: NWConnection?
+ private var sendTask: Task?
+
+ func connect(host: String, port: UInt16) {
+ guard let nwPort = NWEndpoint.Port(rawValue: port) else {
+ print("invalid port")
+ return
+ }
+
+ // connection is in progress
+ isConnected = false
+ isConnecting = true
+
+ connection = NWConnection(host: NWEndpoint.Host(host), port: nwPort, using: .tcp)
+
+ connection?.stateUpdateHandler = { [weak self] state in
+ DispatchQueue.main.async {
+ switch state {
+ case .ready:
+ print("connected")
+ self?.isConnected = true
+ self?.isConnecting = false
+ self?.startSending()
+ case .failed(let error):
+ print("connection fail: \(error)")
+ self?.isConnected = false
+ self?.isConnecting = false
+ case .waiting(let error):
+ // 等待连接中,仍保持 isConnecting = true
+ print("connection waiting: \(error)")
+ case .cancelled:
+ print("connection")
+ self?.isConnected = false
+ self?.isConnecting = false
+ default:
+ break
+ }
+ }
+ }
+
+ connection?.start(queue: .main)
+ }
+
+ func disconnect() {
+ connection?.cancel()
+ connection = nil
+ isConnected = false
+ isConnecting = false
+ stopSending()
+ }
+
+ func send(data: Data) {
+ guard let conn = connection, isConnected else { return }
+ conn.send(content: data, completion: .contentProcessed({ error in
+ if let error = error {
+ print("发送出错: \(error)")
+ }
+ }))
+ }
+
+ private func startSending() {
+ stopSending() // make sure only one sending task is running
+ sendTask = Task {
+ let interval = UInt64(1_000_000_000 / 30) // sample rate 30Hz
+ while !Task.isCancelled {
+ if isConnected {
+ var data = "#"
+ var toSend = false
+ if let phoneData = SensorDataManager.shared.latestPhone?.data.toIMUString() {
+ data += phoneData
+ toSend = true
+ }
+ data += "|"
+ if let watchData = SensorDataManager.shared.latestWatch?.data.toIMUString() {
+ data += watchData
+ toSend = true
+ }
+
+ data += "|"
+ if let headsetData = SensorDataManager.shared.latestHeadset?.data.toIMUString() {
+ data += headsetData
+ toSend = true
+ }
+ data += "\n"
+
+ if toSend {
+ send(data: data.data(using: .utf8)!)
+ }
+ } else {
+ break
+ }
+ try? await Task.sleep(nanoseconds: interval)
+ }
+ }
+ }
+
+ private func stopSending() {
+ sendTask?.cancel()
+ sendTask = nil
+ }
+}
diff --git a/ios_app/phone/WatchConnector.swift b/ios_app/phone/WatchConnector.swift
new file mode 100644
index 0000000..545ffa8
--- /dev/null
+++ b/ios_app/phone/WatchConnector.swift
@@ -0,0 +1,86 @@
+//
+// WatchConnector.swift
+// phone
+//
+// Created by 梁丰洲 on 2024/12/15.
+//
+
+import WatchConnectivity
+
+struct WatchSensorData: Codable {
+ let timestamp: TimeInterval
+ let accX: Double
+ let accY: Double
+ let accZ: Double
+ let gyrX: Double
+ let gyrY: Double
+ let gyrZ: Double
+ let qatX: Double
+ let qatY: Double
+ let qatZ: Double
+ let qatW: Double
+
+ func toSensorData() -> SensorData {
+ return SensorData(
+ accX: accX,
+ accY: accY,
+ accZ: accZ,
+ gyrX: gyrX,
+ gyrY: gyrY,
+ gyrZ: gyrZ,
+ qatX: qatX,
+ qatY: qatY,
+ qatZ: qatZ,
+ qatW: qatW
+ )
+ }
+}
+
+class WatchConnector: NSObject, ObservableObject, WCSessionDelegate {
+ @Published var sensorData: WatchSensorData?
+ let decoder: PropertyListDecoder
+
+ override init() {
+ self.decoder = PropertyListDecoder()
+ super.init()
+ if WCSession.isSupported() {
+ WCSession.default.delegate = self
+ WCSession.default.activate()
+ }
+ }
+
+ func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: (any Error)?) {
+ print("phone activationDidCompleteWith state = \(activationState.rawValue)")
+ }
+
+ func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
+ if let encodedData = message["WATCH_SENSOR_DATA"] as? Data {
+ do {
+ let decodedData = try decoder.decode(WatchSensorData.self, from: encodedData)
+ if sensorData == nil || sensorData!.timestamp < decodedData.timestamp {
+ // schedule main thread
+ DispatchQueue.main.async {
+ self.sensorData = decodedData
+ }
+ SensorDataManager.shared.append(
+ sensorType: .WATCH,
+ data: decodedData.toSensorData(),
+ timestamp: decodedData.timestamp)
+ }
+ } catch {
+ print("Failed to decode sensor data: \(error)")
+ }
+ }
+ }
+
+ func sessionDidBecomeInactive(_ session: WCSession) {
+ print("phone sessionDidBecomeInactive")
+ }
+ func sessionDidDeactivate(_ session: WCSession) {
+ print("phone sessionDidDeactivate")
+ }
+ func sessionWatchStateDidChange(_ session: WCSession) {
+ print("phone sessionWatchStateDidChange")
+
+ }
+}
diff --git a/ios_app/phone/phoneApp.swift b/ios_app/phone/phoneApp.swift
new file mode 100644
index 0000000..356e328
--- /dev/null
+++ b/ios_app/phone/phoneApp.swift
@@ -0,0 +1,17 @@
+//
+// phoneApp.swift
+// phone
+//
+// Created by 梁丰洲 on 2024/12/15.
+//
+
+import SwiftUI
+
+@main
+struct phoneApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}