Decode workout event metadata
This commit is contained in:
parent
b3a902e577
commit
dbe088a402
@ -3,15 +3,23 @@ import SwiftUI
|
|||||||
struct EventDetailView: View {
|
struct EventDetailView: View {
|
||||||
|
|
||||||
let event: WorkoutEvent
|
let event: WorkoutEvent
|
||||||
|
|
||||||
|
var metadata: [(key: String, value: Any)] {
|
||||||
|
event.metadata.sorted { $0.key < $1.key }
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
DetailRow("Date", date: event.date)
|
DetailRow("Date", date: event.date)
|
||||||
DetailRow("Type", value: event.type)
|
DetailRow("Type", value: event.type)
|
||||||
DetailRow("Duration", duration: event.duration)
|
DetailRow("Duration", duration: event.duration)
|
||||||
DetailRow("Metadata", value: event.metadata)
|
|
||||||
DetailRow("Session UUID", value: event.sessionUUID)
|
DetailRow("Session UUID", value: event.sessionUUID)
|
||||||
DetailRow("Error", value: event.error)
|
DetailRow("Error", value: event.error)
|
||||||
|
Section("Metadata") {
|
||||||
|
ForEach(metadata, id: \.key) { (key, value) in
|
||||||
|
DetailRow(key, value: "\(value)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Event")
|
.navigationTitle("Event")
|
||||||
}
|
}
|
||||||
|
@ -42,11 +42,18 @@ extension WorkoutEvent {
|
|||||||
date: Date(timeIntervalSinceReferenceDate: row[columnDate]),
|
date: Date(timeIntervalSinceReferenceDate: row[columnDate]),
|
||||||
type: .init(rawValue: row[columnType])!,
|
type: .init(rawValue: row[columnType])!,
|
||||||
duration: row[columnDuration],
|
duration: row[columnDuration],
|
||||||
metadata: row[columnMetadata],
|
metadata: metadata(row[columnMetadata]),
|
||||||
sessionUUID: row[columnSessionUUID],
|
sessionUUID: row[columnSessionUUID],
|
||||||
error: row[columnError])
|
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 {
|
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.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
|
try database.run(table.create { t in
|
||||||
@ -71,7 +78,7 @@ extension WorkoutEvent {
|
|||||||
columnDate <- element.date.timeIntervalSinceReferenceDate,
|
columnDate <- element.date.timeIntervalSinceReferenceDate,
|
||||||
columnType <- element.type.rawValue,
|
columnType <- element.type.rawValue,
|
||||||
columnDuration <- element.duration,
|
columnDuration <- element.duration,
|
||||||
columnMetadata <- element.metadata,
|
columnMetadata <- encode(metadata: element.metadata),
|
||||||
columnSessionUUID <- element.sessionUUID,
|
columnSessionUUID <- element.sessionUUID,
|
||||||
columnError <- element.error)
|
columnError <- element.error)
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import HealthKit
|
import HealthKit
|
||||||
|
import BinaryCodable
|
||||||
|
|
||||||
struct WorkoutEvent {
|
struct WorkoutEvent {
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ struct WorkoutEvent {
|
|||||||
|
|
||||||
let duration: TimeInterval
|
let duration: TimeInterval
|
||||||
|
|
||||||
let metadata: Data?
|
let metadata: [String : Any]
|
||||||
|
|
||||||
let sessionUUID: Data?
|
let sessionUUID: Data?
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ struct WorkoutEvent {
|
|||||||
extension WorkoutEvent: Equatable {
|
extension WorkoutEvent: Equatable {
|
||||||
|
|
||||||
static func == (lhs: WorkoutEvent, rhs: WorkoutEvent) -> Bool {
|
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) {
|
func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(date)
|
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),
|
.init(date: .init(timeIntervalSinceReferenceDate: 702107518.84307),
|
||||||
type: .init(rawValue: 7)!,
|
type: .init(rawValue: 7)!,
|
||||||
duration: 1114.56374406815,
|
duration: 1114.56374406815,
|
||||||
metadata: .init(hex: mock1Event1Metadata)!,
|
metadata: WorkoutEvent.decode(metadata: .init(hex: mock1Event1Metadata)!),
|
||||||
sessionUUID: nil,
|
sessionUUID: nil,
|
||||||
error: nil),
|
error: nil),
|
||||||
.init(date: .init(timeIntervalSinceReferenceDate: 702107518.84307),
|
.init(date: .init(timeIntervalSinceReferenceDate: 702107518.84307),
|
||||||
type: .init(rawValue: 7)!,
|
type: .init(rawValue: 7)!,
|
||||||
duration: 1972.17168283463,
|
duration: 1972.17168283463,
|
||||||
metadata: .init(hex: mock1Event2Metadata)!,
|
metadata: WorkoutEvent.decode(metadata: .init(hex: mock1Event2Metadata)!),
|
||||||
sessionUUID: nil,
|
sessionUUID: nil,
|
||||||
error: nil),
|
error: nil),
|
||||||
.init(date: .init(timeIntervalSinceReferenceDate: 702112942.707113),
|
.init(date: .init(timeIntervalSinceReferenceDate: 702112942.707113),
|
||||||
type: .init(rawValue: 1)!,
|
type: .init(rawValue: 1)!,
|
||||||
duration: 0.0,
|
duration: 0.0,
|
||||||
metadata: nil,
|
metadata: [:],
|
||||||
sessionUUID: nil,
|
sessionUUID: nil,
|
||||||
error: nil),
|
error: nil),
|
||||||
.init(date: .init(timeIntervalSinceReferenceDate: 702113161.221132),
|
.init(date: .init(timeIntervalSinceReferenceDate: 702113161.221132),
|
||||||
type: .init(rawValue: 2)!,
|
type: .init(rawValue: 2)!,
|
||||||
duration: 0.0,
|
duration: 0.0,
|
||||||
metadata: nil,
|
metadata: [:],
|
||||||
sessionUUID: nil,
|
sessionUUID: nil,
|
||||||
error: nil),
|
error: nil),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let mock1Event1Metadata = "0a370a275f484b507269766174654d65746164617461546f74616c44697374616e63655175616e74697479320c090000000000408f4012016d0a240a205f484b507269766174654d6574616461746149735061727469616c53706c697420000a3d0a2d5f484b507269766174654d6574616461746153706c69744163746976654475726174696f6e5175616e74697479320c098d1f2246416a91401201730a370a275f484b507269766174654d6574616461746153706c697444697374616e63655175616e74697479320c090000000000408f4012016d0a280a245f484b50726976617465576f726b6f75745365676d656e744576656e745375627479706520010a2a0a265f484b507269766174654d6574616461746153706c69744d6561737572696e6753797374656d2001"
|
private let mock1Event1Metadata = "0a370a275f484b507269766174654d65746164617461546f74616c44697374616e63655175616e74697479320c090000000000408f4012016d0a240a205f484b507269766174654d6574616461746149735061727469616c53706c697420000a3d0a2d5f484b507269766174654d6574616461746153706c69744163746976654475726174696f6e5175616e74697479320c098d1f2246416a91401201730a370a275f484b507269766174654d6574616461746153706c697444697374616e63655175616e74697479320c090000000000408f4012016d0a280a245f484b50726976617465576f726b6f75745365676d656e744576656e745375627479706520010a2a0a265f484b507269766174654d6574616461746153706c69744d6561737572696e6753797374656d2001"
|
||||||
|
|
||||||
private let mock1Event2Metadata = "0a370a275f484b507269766174654d65746164617461546f74616c44697374616e63655175616e74697479320c094c3789416025994012016d0a240a205f484b507269766174654d6574616461746149735061727469616c53706c697420000a3d0a2d5f484b507269766174654d6574616461746153706c69744163746976654475726174696f6e5175616e74697479320c09882da1cdafd09e401201730a370a275f484b507269766174654d6574616461746153706c697444697374616e63655175616e74697479320c094c3789416025994012016d0a280a245f484b50726976617465576f726b6f75745365676d656e744576656e745375627479706520010a2a0a265f484b507269766174654d6574616461746153706c69744d6561737572696e6753797374656d2002"
|
private let mock1Event2Metadata = "0a370a275f484b507269766174654d65746164617461546f74616c44697374616e63655175616e74697479320c094c3789416025994012016d0a240a205f484b507269766174654d6574616461746149735061727469616c53706c697420000a3d0a2d5f484b507269766174654d6574616461746153706c69744163746976654475726174696f6e5175616e74697479320c09882da1cdafd09e401201730a370a275f484b507269766174654d6574616461746153706c697444697374616e63655175616e74697479320c094c3789416025994012016d0a280a245f484b50726976617465576f726b6f75745365676d656e744576656e745375627479706520010a2a0a265f484b507269766174654d6574616461746153706c69744d6561737572696e6753797374656d2002"
|
||||||
|
@ -30,7 +30,7 @@ struct WorkoutDetailView: View {
|
|||||||
}
|
}
|
||||||
if !workout.events.isEmpty {
|
if !workout.events.isEmpty {
|
||||||
Section("Events") {
|
Section("Events") {
|
||||||
ForEach(workout.events, id: \.date) { event in
|
ForEach(workout.events) { event in
|
||||||
NavigationLink(value: event) {
|
NavigationLink(value: event) {
|
||||||
DetailRow(event.type.description, date: event.date)
|
DetailRow(event.type.description, date: event.date)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user