File size: 4,593 Bytes
fc93158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import CoreMotion
import Foundation
import OpenClawKit

final class MotionService: MotionServicing {
    func activities(params: OpenClawMotionActivityParams) async throws -> OpenClawMotionActivityPayload {
        guard CMMotionActivityManager.isActivityAvailable() else {
            throw NSError(domain: "Motion", code: 1, userInfo: [
                NSLocalizedDescriptionKey: "MOTION_UNAVAILABLE: activity not supported on this device",
            ])
        }
        let auth = CMMotionActivityManager.authorizationStatus()
        guard auth == .authorized else {
            throw NSError(domain: "Motion", code: 3, userInfo: [
                NSLocalizedDescriptionKey: "MOTION_PERMISSION_REQUIRED: grant Motion & Fitness permission",
            ])
        }

        let (start, end) = Self.resolveRange(startISO: params.startISO, endISO: params.endISO)
        let limit = max(1, min(params.limit ?? 200, 1000))

        let manager = CMMotionActivityManager()
        let mapped: [OpenClawMotionActivityEntry] = try await withCheckedThrowingContinuation { cont in
            manager.queryActivityStarting(from: start, to: end, to: OperationQueue()) { activity, error in
                if let error {
                    cont.resume(throwing: error)
                } else {
                    let formatter = ISO8601DateFormatter()
                    let sliced = Array((activity ?? []).suffix(limit))
                    let entries = sliced.map { entry in
                        OpenClawMotionActivityEntry(
                            startISO: formatter.string(from: entry.startDate),
                            endISO: formatter.string(from: end),
                            confidence: Self.confidenceString(entry.confidence),
                            isWalking: entry.walking,
                            isRunning: entry.running,
                            isCycling: entry.cycling,
                            isAutomotive: entry.automotive,
                            isStationary: entry.stationary,
                            isUnknown: entry.unknown)
                    }
                    cont.resume(returning: entries)
                }
            }
        }

        return OpenClawMotionActivityPayload(activities: mapped)
    }

    func pedometer(params: OpenClawPedometerParams) async throws -> OpenClawPedometerPayload {
        guard CMPedometer.isStepCountingAvailable() else {
            throw NSError(domain: "Motion", code: 2, userInfo: [
                NSLocalizedDescriptionKey: "PEDOMETER_UNAVAILABLE: step counting not supported",
            ])
        }
        let auth = CMPedometer.authorizationStatus()
        guard auth == .authorized else {
            throw NSError(domain: "Motion", code: 4, userInfo: [
                NSLocalizedDescriptionKey: "MOTION_PERMISSION_REQUIRED: grant Motion & Fitness permission",
            ])
        }

        let (start, end) = Self.resolveRange(startISO: params.startISO, endISO: params.endISO)
        let pedometer = CMPedometer()
        let payload: OpenClawPedometerPayload = try await withCheckedThrowingContinuation { cont in
            pedometer.queryPedometerData(from: start, to: end) { data, error in
                if let error {
                    cont.resume(throwing: error)
                } else {
                    let formatter = ISO8601DateFormatter()
                    let payload = OpenClawPedometerPayload(
                        startISO: formatter.string(from: start),
                        endISO: formatter.string(from: end),
                        steps: data?.numberOfSteps.intValue,
                        distanceMeters: data?.distance?.doubleValue,
                        floorsAscended: data?.floorsAscended?.intValue,
                        floorsDescended: data?.floorsDescended?.intValue)
                    cont.resume(returning: payload)
                }
            }
        }
        return payload
    }

    private static func resolveRange(startISO: String?, endISO: String?) -> (Date, Date) {
        let formatter = ISO8601DateFormatter()
        let start = startISO.flatMap { formatter.date(from: $0) } ?? Calendar.current.startOfDay(for: Date())
        let end = endISO.flatMap { formatter.date(from: $0) } ?? Date()
        return (start, end)
    }

    private static func confidenceString(_ confidence: CMMotionActivityConfidence) -> String {
        switch confidence {
        case .low: "low"
        case .medium: "medium"
        case .high: "high"
        @unknown default: "unknown"
        }
    }
}