Decode workout event metadata
This commit is contained in:
parent
b3a902e577
commit
dbe088a402
@ -4,14 +4,22 @@ 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")
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user