Decode workout event metadata

This commit is contained in:
Christoph Hagen 2024-02-01 12:18:49 +01:00
parent b3a902e577
commit dbe088a402
5 changed files with 163 additions and 12 deletions

View File

@ -3,15 +3,23 @@ import SwiftUI
struct EventDetailView: View {
let event: WorkoutEvent
var metadata: [(key: String, value: Any)] {
event.metadata.sorted { $0.key < $1.key }
}
var body: some View {
List {
DetailRow("Date", date: event.date)
DetailRow("Type", value: event.type)
DetailRow("Duration", duration: event.duration)
DetailRow("Metadata", value: event.metadata)
DetailRow("Session UUID", value: event.sessionUUID)
DetailRow("Error", value: event.error)
Section("Metadata") {
ForEach(metadata, id: \.key) { (key, value) in
DetailRow(key, value: "\(value)")
}
}
}
.navigationTitle("Event")
}

View File

@ -42,11 +42,18 @@ extension WorkoutEvent {
date: Date(timeIntervalSinceReferenceDate: row[columnDate]),
type: .init(rawValue: row[columnType])!,
duration: row[columnDuration],
metadata: row[columnMetadata],
metadata: metadata(row[columnMetadata]),
sessionUUID: row[columnSessionUUID],
error: row[columnError])
}
private static func metadata(_ data: Data?) -> [String : Any] {
guard let data else {
return [:]
}
return decode(metadata: data)
}
static func createTable(in database: Database) throws {
// try database.execute("CREATE TABLE workout_events (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, owner_id INTEGER NOT NULL REFERENCES workouts (data_id) ON DELETE CASCADE, date REAL NOT NULL, type INTEGER NOT NULL, duration REAL NOT NULL, metadata BLOB, session_uuid BLOB, error BLOB)")
try database.run(table.create { t in
@ -71,7 +78,7 @@ extension WorkoutEvent {
columnDate <- element.date.timeIntervalSinceReferenceDate,
columnType <- element.type.rawValue,
columnDuration <- element.duration,
columnMetadata <- element.metadata,
columnMetadata <- encode(metadata: element.metadata),
columnSessionUUID <- element.sessionUUID,
columnError <- element.error)
)

View File

@ -1,5 +1,6 @@
import Foundation
import HealthKit
import BinaryCodable
struct WorkoutEvent {
@ -9,7 +10,7 @@ struct WorkoutEvent {
let duration: TimeInterval
let metadata: Data?
let metadata: [String : Any]
let sessionUUID: Data?
@ -20,7 +21,7 @@ struct WorkoutEvent {
extension WorkoutEvent: Equatable {
static func == (lhs: WorkoutEvent, rhs: WorkoutEvent) -> Bool {
lhs.date == rhs.date
lhs.date == rhs.date && lhs.type == rhs.type && lhs.duration == rhs.duration
}
}
@ -35,5 +36,140 @@ extension WorkoutEvent: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(date)
hasher.combine(type.rawValue)
hasher.combine(duration)
}
}
extension WorkoutEvent: Identifiable {
var id: Double {
date.timeIntervalSinceReferenceDate * Double(type.rawValue) * duration
}
}
extension WorkoutEvent {
static func decode(metadata data: Data) -> [String : Any] {
let metadata: WorkoutEventMetadata
do {
metadata = try ProtobufDecoder.decode(WorkoutEventMetadata.self, from: data)
} catch {
print("Failed to decode event metadata: \(error)")
print(data.hex)
return [:]
}
return metadata.elements.reduce(into: [:]) { dict, element in
guard let value = element.value else {
print("No value for metadata element \(element)")
print(data.hex)
return
}
dict[element.key] = value
}
}
static func encode(metadata: [String : Any]) -> Data? {
let wrapper = WorkoutEventMetadata(elements: metadata.compactMap {
.init(key: $0.key, value: $0.value)
})
guard !wrapper.elements.isEmpty else {
return nil
}
do {
return try ProtobufEncoder().encode(wrapper)
} catch {
print("Failed to encode event metadata: \(error)")
return nil
}
}
}
private struct WorkoutEventMetadata {
let elements: [Element]
}
extension WorkoutEventMetadata.Element {
var value: Any? {
if let unsignedValue {
return unsignedValue
}
if let quantity {
return HKQuantity(unit: .init(from: quantity.unit), doubleValue: quantity.value)
}
return UInt(0)
}
init?(key: String, value: Any) {
self.key = key
if let value = value as? UInt {
self.unsignedValue = value
self.quantity = nil
return
}
guard let value = value as? HKQuantity else {
print("Unknown value type for metadata key \(key): \(value)")
return nil
}
self.unsignedValue = nil
if value.is(compatibleWith: .meter()) {
self.quantity = .init(value: value.doubleValue(for: .meter()), unit: "m")
} else if value.is(compatibleWith: .second()) {
self.quantity = .init(value: value.doubleValue(for: .second()), unit: "s")
} else {
print("Unhandled quantity type for metadata key \(key): \(value)")
return nil
}
}
}
extension WorkoutEventMetadata {
struct Element {
let key: String
let unsignedValue: UInt?
let quantity: Quantity?
}
}
extension WorkoutEventMetadata.Element {
struct Quantity {
let value: Double
let unit: String
}
}
extension WorkoutEventMetadata: Codable {
enum CodingKeys: Int, CodingKey {
case elements = 1
}
}
extension WorkoutEventMetadata.Element: Codable {
enum CodingKeys: Int, CodingKey {
case key = 1
case unsignedValue = 4
case quantity = 6
}
}
extension WorkoutEventMetadata.Element.Quantity: Codable {
enum CodingKeys: Int, CodingKey {
case value = 1
case unit = 2
}
}

View File

@ -8,31 +8,31 @@ extension WorkoutEvent {
.init(date: .init(timeIntervalSinceReferenceDate: 702107518.84307),
type: .init(rawValue: 7)!,
duration: 1114.56374406815,
metadata: .init(hex: mock1Event1Metadata)!,
metadata: WorkoutEvent.decode(metadata: .init(hex: mock1Event1Metadata)!),
sessionUUID: nil,
error: nil),
.init(date: .init(timeIntervalSinceReferenceDate: 702107518.84307),
type: .init(rawValue: 7)!,
duration: 1972.17168283463,
metadata: .init(hex: mock1Event2Metadata)!,
metadata: WorkoutEvent.decode(metadata: .init(hex: mock1Event2Metadata)!),
sessionUUID: nil,
error: nil),
.init(date: .init(timeIntervalSinceReferenceDate: 702112942.707113),
type: .init(rawValue: 1)!,
duration: 0.0,
metadata: nil,
metadata: [:],
sessionUUID: nil,
error: nil),
.init(date: .init(timeIntervalSinceReferenceDate: 702113161.221132),
type: .init(rawValue: 2)!,
duration: 0.0,
metadata: nil,
metadata: [:],
sessionUUID: nil,
error: nil),
]
}
}
private let mock1Event1Metadata = "0a370a275f484b507269766174654d65746164617461546f74616c44697374616e63655175616e74697479320c090000000000408f4012016d0a240a205f484b507269766174654d6574616461746149735061727469616c53706c697420000a3d0a2d5f484b507269766174654d6574616461746153706c69744163746976654475726174696f6e5175616e74697479320c098d1f2246416a91401201730a370a275f484b507269766174654d6574616461746153706c697444697374616e63655175616e74697479320c090000000000408f4012016d0a280a245f484b50726976617465576f726b6f75745365676d656e744576656e745375627479706520010a2a0a265f484b507269766174654d6574616461746153706c69744d6561737572696e6753797374656d2001"
private let mock1Event1Metadata = "0a370a275f484b507269766174654d65746164617461546f74616c44697374616e63655175616e74697479320c090000000000408f4012016d0a240a205f484b507269766174654d6574616461746149735061727469616c53706c697420000a3d0a2d5f484b507269766174654d6574616461746153706c69744163746976654475726174696f6e5175616e74697479320c098d1f2246416a91401201730a370a275f484b507269766174654d6574616461746153706c697444697374616e63655175616e74697479320c090000000000408f4012016d0a280a245f484b50726976617465576f726b6f75745365676d656e744576656e745375627479706520010a2a0a265f484b507269766174654d6574616461746153706c69744d6561737572696e6753797374656d2001"
private let mock1Event2Metadata = "0a370a275f484b507269766174654d65746164617461546f74616c44697374616e63655175616e74697479320c094c3789416025994012016d0a240a205f484b507269766174654d6574616461746149735061727469616c53706c697420000a3d0a2d5f484b507269766174654d6574616461746153706c69744163746976654475726174696f6e5175616e74697479320c09882da1cdafd09e401201730a370a275f484b507269766174654d6574616461746153706c697444697374616e63655175616e74697479320c094c3789416025994012016d0a280a245f484b50726976617465576f726b6f75745365676d656e744576656e745375627479706520010a2a0a265f484b507269766174654d6574616461746153706c69744d6561737572696e6753797374656d2002"

View File

@ -30,7 +30,7 @@ struct WorkoutDetailView: View {
}
if !workout.events.isEmpty {
Section("Events") {
ForEach(workout.events, id: \.date) { event in
ForEach(workout.events) { event in
NavigationLink(value: event) {
DetailRow(event.type.description, date: event.date)
}