Files
GameTime/packages/watch_bridge_contract/lib/src/watch_bridge_contract.dart

784 lines
23 KiB
Dart

const int watchBridgeSchemaVersion = 5;
enum WatchCommandType {
startCurrentExercise,
pauseSession,
resumeSession,
startPreparedTimedStep,
completeCurrentStep,
skipCurrentStep,
skipCurrentPassage,
finishCurrentSet,
skipCurrentSet,
skipCurrentRest,
incrementScore,
decrementScore,
}
enum WatchCommandAck {
accepted,
acceptedNoOp,
rejectedStaleRevision,
rejectedNotApplicable,
rejectedNoActiveSession,
rejectedSessionMismatch,
rejectedPhoneBusy,
}
enum WatchSessionPhase {
noActiveSession,
ready,
running,
paused,
nextTimerReady,
restRunning,
restPaused,
betweenSetsReady,
}
enum WatchPrimaryAction {
none,
startCurrentExercise,
pauseSession,
resumeSession,
startPreparedTimedStep,
skipCurrentRest,
}
enum WatchSecondaryAction {
skipCurrentStep,
skipCurrentPassage,
finishCurrentSet,
skipCurrentSet,
skipCurrentRest,
}
enum WatchTimerKind { rest, step, scoreStopwatch, setTimer }
enum WatchTimerDisplayMode { countdown, elapsed }
enum WatchTimerRunState { stopped, running, paused }
enum WatchManualScoreScope { series, step }
enum WatchStepType { time, reps }
final class WatchCommandEnvelope {
const WatchCommandEnvelope({
this.schemaVersion = watchBridgeSchemaVersion,
required this.commandId,
required this.type,
required this.sessionId,
required this.expectedRevision,
required this.sentAtEpochMs,
});
factory WatchCommandEnvelope.fromJson(Map<String, Object?> json) {
return WatchCommandEnvelope(
schemaVersion: _intFromJson(
json['schemaVersion'],
watchBridgeSchemaVersion,
),
commandId: _stringFromJson(json['commandId']),
type: _enumFromJson(
json['type'],
WatchCommandType.values,
WatchCommandType.startCurrentExercise,
),
sessionId: _stringFromJson(json['sessionId']),
expectedRevision: _intFromJson(json['expectedRevision'], 0),
sentAtEpochMs: _intFromJson(json['sentAtEpochMs'], 0),
);
}
final int schemaVersion;
final String commandId;
final WatchCommandType type;
final String sessionId;
final int expectedRevision;
final int sentAtEpochMs;
Map<String, Object?> toJson() {
return {
'schemaVersion': schemaVersion,
'commandId': commandId,
'type': type.name,
'sessionId': sessionId,
'expectedRevision': expectedRevision,
'sentAtEpochMs': sentAtEpochMs,
};
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is WatchCommandEnvelope &&
schemaVersion == other.schemaVersion &&
commandId == other.commandId &&
type == other.type &&
sessionId == other.sessionId &&
expectedRevision == other.expectedRevision &&
sentAtEpochMs == other.sentAtEpochMs;
}
@override
int get hashCode {
return Object.hash(
schemaVersion,
commandId,
type,
sessionId,
expectedRevision,
sentAtEpochMs,
);
}
}
final class WatchSessionProjection {
const WatchSessionProjection({
this.schemaVersion = watchBridgeSchemaVersion,
required this.deviceSessionId,
required this.revision,
required this.projectedAtEpochMs,
this.expiresAtEpochMs = 0,
required this.phase,
required this.phoneReachable,
required this.seriesIndex,
required this.seriesTotal,
required this.exerciseName,
this.programIndex,
this.exerciseIndex,
this.setIndex,
this.passageIndex,
this.passageTotal,
this.stepIndex,
this.stepTotal,
this.stepName,
this.stepType,
this.stepTargetValue,
this.dominantTimer,
this.secondaryTimers = const [],
required this.primaryAction,
this.secondaryActions = const [],
this.nextExerciseName,
this.statusLabel,
this.hasManualScore = false,
this.currentManualScoreValue,
this.canDecrementScore = false,
this.manualScoreTargetValue,
this.manualScoreTargetLabel,
this.manualScoreRepsTargetValue,
this.manualScoreScope,
});
factory WatchSessionProjection.fromJson(Map<String, Object?> json) {
return WatchSessionProjection(
schemaVersion: _intFromJson(
json['schemaVersion'],
watchBridgeSchemaVersion,
),
deviceSessionId: _stringFromJson(json['deviceSessionId']),
revision: _intFromJson(json['revision'], 0),
projectedAtEpochMs: _intFromJson(json['projectedAtEpochMs'], 0),
expiresAtEpochMs: _intFromJson(json['expiresAtEpochMs'], 0),
phase: _enumFromJson(
json['phase'],
WatchSessionPhase.values,
WatchSessionPhase.noActiveSession,
),
phoneReachable: _boolFromJson(json['phoneReachable'], false),
seriesIndex: _intFromJson(json['seriesIndex'], 0),
seriesTotal: _intFromJson(json['seriesTotal'], 0),
exerciseName: _stringFromJson(json['exerciseName']),
programIndex: _nullableIntFromJson(json['programIndex']),
exerciseIndex: _nullableIntFromJson(json['exerciseIndex']),
setIndex: _nullableIntFromJson(json['setIndex']),
passageIndex: _nullableIntFromJson(json['passageIndex']),
passageTotal: _nullableIntFromJson(json['passageTotal']),
stepIndex: _nullableIntFromJson(json['stepIndex']),
stepTotal: _nullableIntFromJson(json['stepTotal']),
stepName: _nullableStringFromJson(json['stepName']),
stepType: _nullableEnumFromJson(json['stepType'], WatchStepType.values),
stepTargetValue: _nullableIntFromJson(json['stepTargetValue']),
dominantTimer: _timerFromJson(json['dominantTimer']),
secondaryTimers: _timerListFromJson(json['secondaryTimers']),
primaryAction: _enumFromJson(
json['primaryAction'],
WatchPrimaryAction.values,
WatchPrimaryAction.none,
),
secondaryActions: _enumListFromJson(
json['secondaryActions'],
WatchSecondaryAction.values,
),
nextExerciseName: _nullableStringFromJson(json['nextExerciseName']),
statusLabel: _nullableStringFromJson(json['statusLabel']),
hasManualScore: _boolFromJson(json['hasManualScore'], false),
currentManualScoreValue: _nullableDoubleFromJson(
json['currentManualScoreValue'],
),
canDecrementScore: _boolFromJson(json['canDecrementScore'], false),
manualScoreTargetValue: _nullableDoubleFromJson(
json['manualScoreTargetValue'],
),
manualScoreTargetLabel: _nullableStringFromJson(
json['manualScoreTargetLabel'],
),
manualScoreRepsTargetValue: _nullableIntFromJson(
json['manualScoreRepsTargetValue'],
),
manualScoreScope: _nullableEnumFromJson(
json['manualScoreScope'],
WatchManualScoreScope.values,
),
);
}
final int schemaVersion;
final String deviceSessionId;
final int revision;
final int projectedAtEpochMs;
final int expiresAtEpochMs;
final WatchSessionPhase phase;
final bool phoneReachable;
final int seriesIndex;
final int seriesTotal;
final String exerciseName;
final int? programIndex;
final int? exerciseIndex;
final int? setIndex;
final int? passageIndex;
final int? passageTotal;
final int? stepIndex;
final int? stepTotal;
final String? stepName;
final WatchStepType? stepType;
final int? stepTargetValue;
final WatchTimerProjection? dominantTimer;
final List<WatchTimerProjection> secondaryTimers;
final WatchPrimaryAction primaryAction;
final List<WatchSecondaryAction> secondaryActions;
final String? nextExerciseName;
final String? statusLabel;
final bool hasManualScore;
final double? currentManualScoreValue;
final bool canDecrementScore;
final double? manualScoreTargetValue;
final String? manualScoreTargetLabel;
final int? manualScoreRepsTargetValue;
final WatchManualScoreScope? manualScoreScope;
Map<String, Object?> toJson() {
return {
'schemaVersion': schemaVersion,
'deviceSessionId': deviceSessionId,
'revision': revision,
'projectedAtEpochMs': projectedAtEpochMs,
'expiresAtEpochMs': expiresAtEpochMs,
'phase': phase.name,
'phoneReachable': phoneReachable,
'seriesIndex': seriesIndex,
'seriesTotal': seriesTotal,
'exerciseName': exerciseName,
'programIndex': programIndex,
'exerciseIndex': exerciseIndex,
'setIndex': setIndex,
'passageIndex': passageIndex,
'passageTotal': passageTotal,
'stepIndex': stepIndex,
'stepTotal': stepTotal,
'stepName': stepName,
'stepType': stepType?.name,
'stepTargetValue': stepTargetValue,
'dominantTimer': dominantTimer?.toJson(),
'secondaryTimers': secondaryTimers
.map((timer) => timer.toJson())
.toList(),
'primaryAction': primaryAction.name,
'secondaryActions': secondaryActions
.map((action) => action.name)
.toList(),
'nextExerciseName': nextExerciseName,
'statusLabel': statusLabel,
'hasManualScore': hasManualScore,
'currentManualScoreValue': currentManualScoreValue,
'canDecrementScore': canDecrementScore,
'manualScoreTargetValue': manualScoreTargetValue,
'manualScoreTargetLabel': manualScoreTargetLabel,
'manualScoreRepsTargetValue': manualScoreRepsTargetValue,
'manualScoreScope': manualScoreScope?.name,
};
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is WatchSessionProjection &&
schemaVersion == other.schemaVersion &&
deviceSessionId == other.deviceSessionId &&
revision == other.revision &&
projectedAtEpochMs == other.projectedAtEpochMs &&
expiresAtEpochMs == other.expiresAtEpochMs &&
phase == other.phase &&
phoneReachable == other.phoneReachable &&
seriesIndex == other.seriesIndex &&
seriesTotal == other.seriesTotal &&
exerciseName == other.exerciseName &&
programIndex == other.programIndex &&
exerciseIndex == other.exerciseIndex &&
setIndex == other.setIndex &&
passageIndex == other.passageIndex &&
passageTotal == other.passageTotal &&
stepIndex == other.stepIndex &&
stepTotal == other.stepTotal &&
stepName == other.stepName &&
stepType == other.stepType &&
stepTargetValue == other.stepTargetValue &&
dominantTimer == other.dominantTimer &&
_listEquals(secondaryTimers, other.secondaryTimers) &&
primaryAction == other.primaryAction &&
_listEquals(secondaryActions, other.secondaryActions) &&
nextExerciseName == other.nextExerciseName &&
statusLabel == other.statusLabel &&
hasManualScore == other.hasManualScore &&
currentManualScoreValue == other.currentManualScoreValue &&
canDecrementScore == other.canDecrementScore &&
manualScoreTargetValue == other.manualScoreTargetValue &&
manualScoreTargetLabel == other.manualScoreTargetLabel &&
manualScoreRepsTargetValue == other.manualScoreRepsTargetValue &&
manualScoreScope == other.manualScoreScope;
}
@override
int get hashCode {
return Object.hashAll([
schemaVersion,
deviceSessionId,
revision,
projectedAtEpochMs,
expiresAtEpochMs,
phase,
phoneReachable,
seriesIndex,
seriesTotal,
exerciseName,
programIndex,
exerciseIndex,
setIndex,
passageIndex,
passageTotal,
stepIndex,
stepTotal,
stepName,
stepType,
stepTargetValue,
dominantTimer,
Object.hashAll(secondaryTimers),
primaryAction,
Object.hashAll(secondaryActions),
nextExerciseName,
statusLabel,
hasManualScore,
currentManualScoreValue,
canDecrementScore,
manualScoreTargetValue,
manualScoreTargetLabel,
manualScoreRepsTargetValue,
manualScoreScope,
]);
}
}
final class WatchSensorSummary {
const WatchSensorSummary({
this.schemaVersion = watchBridgeSchemaVersion,
required this.sessionId,
required this.sampleCount,
this.minHeartRateBpm,
this.averageHeartRateBpm,
this.maxHeartRateBpm,
this.totalDistanceMeters,
this.totalCaloriesKcal,
});
factory WatchSensorSummary.fromJson(Map<String, Object?> json) {
return WatchSensorSummary(
schemaVersion: _intFromJson(
json['schemaVersion'],
watchBridgeSchemaVersion,
),
sessionId: _stringFromJson(json['sessionId']),
sampleCount: _intFromJson(json['sampleCount'], 0),
minHeartRateBpm: _nullableIntFromJson(json['minHeartRateBpm']),
averageHeartRateBpm: _nullableDoubleFromJson(json['averageHeartRateBpm']),
maxHeartRateBpm: _nullableIntFromJson(json['maxHeartRateBpm']),
totalDistanceMeters: _nullableDoubleFromJson(json['totalDistanceMeters']),
totalCaloriesKcal: _nullableDoubleFromJson(json['totalCaloriesKcal']),
);
}
final int schemaVersion;
final String sessionId;
final int sampleCount;
final int? minHeartRateBpm;
final double? averageHeartRateBpm;
final int? maxHeartRateBpm;
final double? totalDistanceMeters;
final double? totalCaloriesKcal;
Map<String, Object?> toJson() {
return {
'schemaVersion': schemaVersion,
'sessionId': sessionId,
'sampleCount': sampleCount,
'minHeartRateBpm': minHeartRateBpm,
'averageHeartRateBpm': averageHeartRateBpm,
'maxHeartRateBpm': maxHeartRateBpm,
'totalDistanceMeters': totalDistanceMeters,
'totalCaloriesKcal': totalCaloriesKcal,
};
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is WatchSensorSummary &&
schemaVersion == other.schemaVersion &&
sessionId == other.sessionId &&
sampleCount == other.sampleCount &&
minHeartRateBpm == other.minHeartRateBpm &&
averageHeartRateBpm == other.averageHeartRateBpm &&
maxHeartRateBpm == other.maxHeartRateBpm &&
totalDistanceMeters == other.totalDistanceMeters &&
totalCaloriesKcal == other.totalCaloriesKcal;
}
@override
int get hashCode {
return Object.hash(
schemaVersion,
sessionId,
sampleCount,
minHeartRateBpm,
averageHeartRateBpm,
maxHeartRateBpm,
totalDistanceMeters,
totalCaloriesKcal,
);
}
}
final class WatchSensorSample {
const WatchSensorSample({
this.schemaVersion = watchBridgeSchemaVersion,
this.sampleId,
required this.sessionId,
int? capturedAtEpochMs,
int? recordedAtEpochMs,
this.programIndex,
this.exerciseIndex,
this.setIndex,
this.passageIndex,
this.stepIndex,
this.programSnapshotId,
this.exerciseSnapshotId,
this.stepSnapshotId,
this.heartRateBpm,
this.distanceMeters,
this.caloriesKcal,
}) : capturedAtEpochMs = capturedAtEpochMs ?? recordedAtEpochMs ?? 0;
factory WatchSensorSample.fromJson(Map<String, Object?> json) {
return WatchSensorSample(
schemaVersion: _intFromJson(
json['schemaVersion'],
watchBridgeSchemaVersion,
),
sampleId: _nullableStringFromJson(json['sampleId']),
sessionId: _stringFromJson(json['sessionId']),
capturedAtEpochMs: _intFromJson(
json['capturedAtEpochMs'],
_intFromJson(json['recordedAtEpochMs'], 0),
),
programIndex: _nullableIntFromJson(json['programIndex']),
exerciseIndex: _nullableIntFromJson(json['exerciseIndex']),
setIndex: _nullableIntFromJson(json['setIndex']),
passageIndex: _nullableIntFromJson(json['passageIndex']),
stepIndex: _nullableIntFromJson(json['stepIndex']),
programSnapshotId: _nullableStringFromJson(json['programSnapshotId']),
exerciseSnapshotId: _nullableStringFromJson(json['exerciseSnapshotId']),
stepSnapshotId: _nullableStringFromJson(json['stepSnapshotId']),
heartRateBpm: _nullableIntFromJson(json['heartRateBpm']),
distanceMeters: _nullableDoubleFromJson(json['distanceMeters']),
caloriesKcal: _nullableDoubleFromJson(json['caloriesKcal']),
);
}
final int schemaVersion;
final String? sampleId;
final String sessionId;
final int capturedAtEpochMs;
final int? programIndex;
final int? exerciseIndex;
final int? setIndex;
final int? passageIndex;
final int? stepIndex;
final String? programSnapshotId;
final String? exerciseSnapshotId;
final String? stepSnapshotId;
final int? heartRateBpm;
final double? distanceMeters;
final double? caloriesKcal;
int get recordedAtEpochMs => capturedAtEpochMs;
Map<String, Object?> toJson() {
return {
'schemaVersion': schemaVersion,
'sampleId': sampleId,
'sessionId': sessionId,
'capturedAtEpochMs': capturedAtEpochMs,
'recordedAtEpochMs': capturedAtEpochMs,
'programIndex': programIndex,
'exerciseIndex': exerciseIndex,
'setIndex': setIndex,
'passageIndex': passageIndex,
'stepIndex': stepIndex,
'programSnapshotId': programSnapshotId,
'exerciseSnapshotId': exerciseSnapshotId,
'stepSnapshotId': stepSnapshotId,
'heartRateBpm': heartRateBpm,
'distanceMeters': distanceMeters,
'caloriesKcal': caloriesKcal,
};
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is WatchSensorSample &&
schemaVersion == other.schemaVersion &&
sampleId == other.sampleId &&
sessionId == other.sessionId &&
capturedAtEpochMs == other.capturedAtEpochMs &&
programIndex == other.programIndex &&
exerciseIndex == other.exerciseIndex &&
setIndex == other.setIndex &&
passageIndex == other.passageIndex &&
stepIndex == other.stepIndex &&
programSnapshotId == other.programSnapshotId &&
exerciseSnapshotId == other.exerciseSnapshotId &&
stepSnapshotId == other.stepSnapshotId &&
heartRateBpm == other.heartRateBpm &&
distanceMeters == other.distanceMeters &&
caloriesKcal == other.caloriesKcal;
}
@override
int get hashCode {
return Object.hash(
schemaVersion,
sampleId,
sessionId,
capturedAtEpochMs,
programIndex,
exerciseIndex,
setIndex,
passageIndex,
stepIndex,
programSnapshotId,
exerciseSnapshotId,
stepSnapshotId,
heartRateBpm,
distanceMeters,
caloriesKcal,
);
}
}
typedef WatchTelemetrySummary = WatchSensorSummary;
typedef WatchTelemetrySample = WatchSensorSample;
final class WatchTimerProjection {
const WatchTimerProjection({
required this.kind,
required this.label,
required this.displayMode,
required this.runState,
required this.referenceEpochMs,
required this.accumulatedMs,
this.startedAtEpochMs,
this.targetMs,
});
factory WatchTimerProjection.fromJson(Map<String, Object?> json) {
return WatchTimerProjection(
kind: _enumFromJson(
json['kind'],
WatchTimerKind.values,
WatchTimerKind.setTimer,
),
label: _stringFromJson(json['label']),
displayMode: _enumFromJson(
json['displayMode'],
WatchTimerDisplayMode.values,
WatchTimerDisplayMode.elapsed,
),
runState: _enumFromJson(
json['runState'],
WatchTimerRunState.values,
WatchTimerRunState.stopped,
),
referenceEpochMs: _intFromJson(json['referenceEpochMs'], 0),
accumulatedMs: _intFromJson(json['accumulatedMs'], 0),
startedAtEpochMs: _nullableIntFromJson(json['startedAtEpochMs']),
targetMs: _nullableIntFromJson(json['targetMs']),
);
}
final WatchTimerKind kind;
final String label;
final WatchTimerDisplayMode displayMode;
final WatchTimerRunState runState;
final int referenceEpochMs;
final int accumulatedMs;
final int? startedAtEpochMs;
final int? targetMs;
Map<String, Object?> toJson() {
return {
'kind': kind.name,
'label': label,
'displayMode': displayMode.name,
'runState': runState.name,
'referenceEpochMs': referenceEpochMs,
'accumulatedMs': accumulatedMs,
'startedAtEpochMs': startedAtEpochMs,
'targetMs': targetMs,
};
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is WatchTimerProjection &&
kind == other.kind &&
label == other.label &&
displayMode == other.displayMode &&
runState == other.runState &&
referenceEpochMs == other.referenceEpochMs &&
accumulatedMs == other.accumulatedMs &&
startedAtEpochMs == other.startedAtEpochMs &&
targetMs == other.targetMs;
}
@override
int get hashCode {
return Object.hash(
kind,
label,
displayMode,
runState,
referenceEpochMs,
accumulatedMs,
startedAtEpochMs,
targetMs,
);
}
}
T _enumFromJson<T extends Enum>(Object? value, List<T> values, T fallback) {
if (value is String) {
for (final enumValue in values) {
if (enumValue.name == value) {
return enumValue;
}
}
}
return fallback;
}
T? _nullableEnumFromJson<T extends Enum>(Object? value, List<T> values) {
if (value is String) {
for (final enumValue in values) {
if (enumValue.name == value) {
return enumValue;
}
}
}
return null;
}
List<T> _enumListFromJson<T extends Enum>(Object? value, List<T> values) {
if (value is! List) {
return const [];
}
return [
for (final item in value)
if (item is String)
for (final enumValue in values)
if (enumValue.name == item) enumValue,
];
}
WatchTimerProjection? _timerFromJson(Object? value) {
if (value is! Map) {
return null;
}
return WatchTimerProjection.fromJson(Map<String, Object?>.from(value));
}
List<WatchTimerProjection> _timerListFromJson(Object? value) {
if (value is! List) {
return const [];
}
return [
for (final item in value)
if (item is Map)
WatchTimerProjection.fromJson(Map<String, Object?>.from(item)),
];
}
String _stringFromJson(Object? value) {
return value is String ? value : '';
}
String? _nullableStringFromJson(Object? value) {
return value is String ? value : null;
}
int _intFromJson(Object? value, int fallback) {
return value is int ? value : fallback;
}
int? _nullableIntFromJson(Object? value) {
return value is int ? value : null;
}
double? _nullableDoubleFromJson(Object? value) {
return switch (value) {
double() => value,
int() => value.toDouble(),
_ => null,
};
}
bool _boolFromJson(Object? value, bool fallback) {
return value is bool ? value : fallback;
}
bool _listEquals<T>(List<T> left, List<T> right) {
if (identical(left, right)) {
return true;
}
if (left.length != right.length) {
return false;
}
for (var index = 0; index < left.length; index += 1) {
if (left[index] != right[index]) {
return false;
}
}
return true;
}