|
|
|
import 'dart:math' as math;
|
|
|
|
|
|
|
|
import '../../../../pigeon/health_kit_raw_data_api.g.dart';
|
|
|
|
import '../health_raw_models.dart';
|
|
|
|
|
|
|
|
class HuaweiHealthRawStressCalculator {
|
|
|
|
const HuaweiHealthRawStressCalculator({required this.userId});
|
|
|
|
|
|
|
|
static const int rawRmssdMin = 5;
|
|
|
|
static const int rawRmssdMax = 200;
|
|
|
|
static const int baselineSampleCount = 50;
|
|
|
|
static const double defaultBaselineAwakeRmssd = 50;
|
|
|
|
static const double trendReference = 50;
|
|
|
|
static const double huaweiTrendScaleFactorAwake = 1;
|
|
|
|
static const double huaweiTrendScaleFactorSleep = 1;
|
|
|
|
|
|
|
|
final int userId;
|
|
|
|
|
|
|
|
HealthRawStressCalculationResult calculate({
|
|
|
|
required List<HealthKitRawDataPoint> hrvPoints,
|
|
|
|
required List<HealthKitRawDataPoint> heartRatePoints,
|
|
|
|
required List<HealthKitRawDataPoint> restingHeartRatePoints,
|
|
|
|
List<HealthKitRawDataPoint> sleepIntervals =
|
|
|
|
const <HealthKitRawDataPoint>[],
|
|
|
|
List<HealthKitRawWorkoutDataPoint> workoutIntervals =
|
|
|
|
const <HealthKitRawWorkoutDataPoint>[],
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
}) {
|
|
|
|
final marked = _markRawData(
|
|
|
|
hrvPoints: hrvPoints,
|
|
|
|
heartRatePoints: heartRatePoints,
|
|
|
|
restingHeartRatePoints: restingHeartRatePoints,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
workoutIntervals: workoutIntervals,
|
|
|
|
);
|
|
|
|
final sortedRmssd = marked.hrv.where((e) {
|
|
|
|
final value = e.value;
|
|
|
|
return value != null && _isValidRmssd(value);
|
|
|
|
}).toList()
|
|
|
|
..sort((a, b) => a.endTime.compareTo(b.endTime));
|
|
|
|
final sortedHr = marked.heartRate.where((e) {
|
|
|
|
final value = e.value;
|
|
|
|
return value != null && _isValidHr(value);
|
|
|
|
}).toList()
|
|
|
|
..sort((a, b) => a.endTime.compareTo(b.endTime));
|
|
|
|
final sortedRestingHr = marked.restingHeartRate.where((e) {
|
|
|
|
final value = e.value;
|
|
|
|
return value != null && _isValidHr(value);
|
|
|
|
}).toList()
|
|
|
|
..sort((a, b) => a.endTime.compareTo(b.endTime));
|
|
|
|
|
|
|
|
final hrvStressPoints = _calculateHrvStressPoints(
|
|
|
|
rmssdPoints: sortedRmssd,
|
|
|
|
heartRatePoints: sortedHr,
|
|
|
|
restingHeartRatePoints: sortedRestingHr,
|
|
|
|
);
|
|
|
|
final realtimeStressPoints = _calculateRealtimeStressPoints(
|
|
|
|
heartRatePoints: sortedHr,
|
|
|
|
restingHeartRatePoints: sortedRestingHr,
|
|
|
|
hrvStressPoints: hrvStressPoints,
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
);
|
|
|
|
|
|
|
|
return HealthRawStressCalculationResult(
|
|
|
|
userId: userId,
|
|
|
|
hrvStressPoints: hrvStressPoints,
|
|
|
|
realtimeStressPoints: realtimeStressPoints,
|
|
|
|
dailyStressPoints: calculateDailyStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
realtimePoints: realtimeStressPoints,
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
dataTime: endTime,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static List<HealthRawDailyStressPoint> calculateDailyStressPoints({
|
|
|
|
required int userId,
|
|
|
|
required List<HealthRawRealtimeStressPoint> realtimePoints,
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
required int dataTime,
|
|
|
|
Set<int> skipExistingDates = const <int>{},
|
|
|
|
}) {
|
|
|
|
if (realtimePoints.isEmpty) return const <HealthRawDailyStressPoint>[];
|
|
|
|
final dates = <int>{
|
|
|
|
for (final point in realtimePoints)
|
|
|
|
if (point.rawEndTime >= startTime && point.rawEndTime <= endTime)
|
|
|
|
_dateKeyFromUnixSeconds(point.rawEndTime),
|
|
|
|
}.toList()
|
|
|
|
..sort();
|
|
|
|
final dailyStressPoints = <HealthRawDailyStressPoint>[];
|
|
|
|
for (final date in dates) {
|
|
|
|
if (skipExistingDates.contains(date)) continue;
|
|
|
|
final (dayStart, dayEnd) = _dayRangeFromDateKey(date);
|
|
|
|
final dayRealtimePoints = realtimePoints
|
|
|
|
.where((point) =>
|
|
|
|
point.rawEndTime >= dayStart && point.rawEndTime <= dayEnd)
|
|
|
|
.toList();
|
|
|
|
final point = HealthRawDailyStressCalculator.calculate(
|
|
|
|
userId: userId,
|
|
|
|
date: date,
|
|
|
|
realtimePoints: dayRealtimePoints,
|
|
|
|
dataTime: dataTime,
|
|
|
|
);
|
|
|
|
if (point != null) dailyStressPoints.add(point);
|
|
|
|
}
|
|
|
|
return dailyStressPoints;
|
|
|
|
}
|
|
|
|
|
|
|
|
HealthRawMarkedDataSet _markRawData({
|
|
|
|
required List<HealthKitRawDataPoint> hrvPoints,
|
|
|
|
required List<HealthKitRawDataPoint> heartRatePoints,
|
|
|
|
required List<HealthKitRawDataPoint> restingHeartRatePoints,
|
|
|
|
required List<HealthKitRawDataPoint> sleepIntervals,
|
|
|
|
required List<HealthKitRawWorkoutDataPoint> workoutIntervals,
|
|
|
|
}) {
|
|
|
|
final hrv = hrvPoints
|
|
|
|
.map((point) => HealthRawMarkedDataPoint(
|
|
|
|
raw: point,
|
|
|
|
flags: _flagsForPoint(
|
|
|
|
point,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
workoutIntervals: workoutIntervals,
|
|
|
|
),
|
|
|
|
))
|
|
|
|
.toList()
|
|
|
|
..sort((a, b) => a.endTime.compareTo(b.endTime));
|
|
|
|
final heartRate = heartRatePoints
|
|
|
|
.map((point) => HealthRawMarkedDataPoint(
|
|
|
|
raw: point,
|
|
|
|
flags: _flagsForPoint(
|
|
|
|
point,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
workoutIntervals: workoutIntervals,
|
|
|
|
),
|
|
|
|
))
|
|
|
|
.toList()
|
|
|
|
..sort((a, b) => a.endTime.compareTo(b.endTime));
|
|
|
|
final restingHeartRate = restingHeartRatePoints
|
|
|
|
.map((point) => HealthRawMarkedDataPoint(
|
|
|
|
raw: point,
|
|
|
|
flags: _flagsForPoint(
|
|
|
|
point,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
workoutIntervals: workoutIntervals,
|
|
|
|
),
|
|
|
|
))
|
|
|
|
.toList()
|
|
|
|
..sort((a, b) => a.endTime.compareTo(b.endTime));
|
|
|
|
|
|
|
|
return HealthRawMarkedDataSet(
|
|
|
|
hrv: hrv,
|
|
|
|
heartRate: heartRate,
|
|
|
|
restingHeartRate: restingHeartRate,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
HealthRawPointFlags _flagsForPoint(
|
|
|
|
HealthKitRawDataPoint point, {
|
|
|
|
required List<HealthKitRawDataPoint> sleepIntervals,
|
|
|
|
required List<HealthKitRawWorkoutDataPoint> workoutIntervals,
|
|
|
|
}) {
|
|
|
|
final isWorkout = workoutIntervals.any(
|
|
|
|
(workout) =>
|
|
|
|
point.endTime > workout.startTime && point.endTime <= workout.endTime,
|
|
|
|
);
|
|
|
|
final isWorkoutRecovery = !isWorkout &&
|
|
|
|
workoutIntervals.any(
|
|
|
|
(workout) =>
|
|
|
|
point.endTime > workout.endTime &&
|
|
|
|
point.endTime <= workout.endTime + 30 * 60,
|
|
|
|
);
|
|
|
|
return HealthRawPointFlags(
|
|
|
|
isWorkout: isWorkout,
|
|
|
|
isWorkoutRecovery: isWorkoutRecovery,
|
|
|
|
isSleepLikely: sleepIntervals.any(
|
|
|
|
(sleep) =>
|
|
|
|
point.endTime > sleep.startTime && point.endTime <= sleep.endTime,
|
|
|
|
),
|
|
|
|
isSuspectedActivity: point.isMotionLike == true,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
List<HealthRawHrvStressPoint> _calculateHrvStressPoints({
|
|
|
|
required List<HealthRawMarkedDataPoint> rmssdPoints,
|
|
|
|
required List<HealthRawMarkedDataPoint> heartRatePoints,
|
|
|
|
required List<HealthRawMarkedDataPoint> restingHeartRatePoints,
|
|
|
|
}) {
|
|
|
|
final validRmssdHistory = <HealthRawMarkedDataPoint>[];
|
|
|
|
final results = <HealthRawHrvStressPoint>[];
|
|
|
|
|
|
|
|
for (final point in rmssdPoints) {
|
|
|
|
final pointValue = point.value!;
|
|
|
|
final isActivity = point.flags.isWorkout || point.flags.isWorkoutRecovery;
|
|
|
|
if (isActivity) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
final isSleep = point.flags.isSleepLikely;
|
|
|
|
final awakeRmssdHistory = _latestBefore(
|
|
|
|
validRmssdHistory
|
|
|
|
.where((e) =>
|
|
|
|
e.flags.isAwakeBaselineEligible && _isValidRmssd(e.value))
|
|
|
|
.toList(),
|
|
|
|
point.endTime,
|
|
|
|
limit: baselineSampleCount,
|
|
|
|
);
|
|
|
|
final sleepRmssdHistory = _latestBefore(
|
|
|
|
validRmssdHistory
|
|
|
|
.where((e) =>
|
|
|
|
e.flags.isSleepHrvBaselineEligible && _isValidRmssd(e.value))
|
|
|
|
.toList(),
|
|
|
|
point.endTime,
|
|
|
|
limit: baselineSampleCount,
|
|
|
|
);
|
|
|
|
final baselineAwakeRmssd = _baseline(
|
|
|
|
awakeRmssdHistory.map((e) => e.value!).toList(),
|
|
|
|
requiredCount: baselineSampleCount,
|
|
|
|
fallback: defaultBaselineAwakeRmssd,
|
|
|
|
);
|
|
|
|
final baselineSleepRmssd = _baseline(
|
|
|
|
sleepRmssdHistory.map((e) => e.value!).toList(),
|
|
|
|
requiredCount: baselineSampleCount,
|
|
|
|
fallback: baselineAwakeRmssd,
|
|
|
|
);
|
|
|
|
final baselineRmssd = isSleep ? baselineSleepRmssd : baselineAwakeRmssd;
|
|
|
|
final referenceHrv = isSleep
|
|
|
|
? trendReference * huaweiTrendScaleFactorSleep
|
|
|
|
: trendReference * huaweiTrendScaleFactorAwake;
|
|
|
|
final rmssdRatio = pointValue / baselineRmssd;
|
|
|
|
final trendDelta = referenceHrv * (rmssdRatio - 1);
|
|
|
|
final trendBaseValue = pointValue >= baselineRmssd
|
|
|
|
? referenceHrv + trendDelta * 0.8
|
|
|
|
: referenceHrv + trendDelta;
|
|
|
|
|
|
|
|
final restingHistory = _latestBefore(
|
|
|
|
restingHeartRatePoints
|
|
|
|
.where((e) => e.flags.isAwakeHrBaselineEligible)
|
|
|
|
.toList(),
|
|
|
|
point.endTime,
|
|
|
|
limit: baselineSampleCount,
|
|
|
|
);
|
|
|
|
final baselineRestingHr = _baseline(
|
|
|
|
restingHistory.map((e) => e.value!).toList(),
|
|
|
|
requiredCount: baselineSampleCount,
|
|
|
|
fallback: 65,
|
|
|
|
);
|
|
|
|
final hrWindow = isSleep
|
|
|
|
? const <HealthRawMarkedDataPoint>[]
|
|
|
|
: _pointsInRange(
|
|
|
|
heartRatePoints,
|
|
|
|
point.endTime - 5 * 60,
|
|
|
|
point.endTime,
|
|
|
|
).where((e) => e.flags.isCurrentAwakeHrEligible).toList();
|
|
|
|
final currentHr = _median(hrWindow.map((e) => e.value!).toList());
|
|
|
|
|
|
|
|
final double trendHrv;
|
|
|
|
if (currentHr != null) {
|
|
|
|
final hrRatio = currentHr / baselineRestingHr;
|
|
|
|
final double hrFactor;
|
|
|
|
if (hrRatio >= 1) {
|
|
|
|
hrFactor = _clamp(1 - 0.5 * (hrRatio - 1), 0.75, 1.10);
|
|
|
|
} else if (pointValue >= baselineRmssd) {
|
|
|
|
hrFactor = _clamp(1 + 0.3 * (1 - hrRatio), 0.75, 1.10);
|
|
|
|
} else {
|
|
|
|
hrFactor = 1;
|
|
|
|
}
|
|
|
|
final mismatch = math.max(hrRatio - rmssdRatio, 0).toDouble();
|
|
|
|
final matchFactor = _clamp(1 - 0.5 * mismatch, 0.90, 1.00);
|
|
|
|
trendHrv = _clamp(
|
|
|
|
trendBaseValue * hrFactor * matchFactor,
|
|
|
|
5,
|
|
|
|
referenceHrv * 1.8,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
trendHrv = _clamp(trendBaseValue, 5, referenceHrv * 1.8);
|
|
|
|
}
|
|
|
|
|
|
|
|
final sourceRange = _timeRange([
|
|
|
|
...awakeRmssdHistory,
|
|
|
|
...sleepRmssdHistory,
|
|
|
|
point,
|
|
|
|
...hrWindow,
|
|
|
|
...restingHistory,
|
|
|
|
]);
|
|
|
|
final normalizedTrendHrv = _integerDouble(trendHrv);
|
|
|
|
results.add(
|
|
|
|
HealthRawHrvStressPoint(
|
|
|
|
userId: userId,
|
|
|
|
rawEndTime: point.endTime,
|
|
|
|
rawHrv: pointValue,
|
|
|
|
result: normalizedTrendHrv,
|
|
|
|
sourceStartTime: sourceRange.start,
|
|
|
|
sourceEndTime: sourceRange.end,
|
|
|
|
state: _hrvState(normalizedTrendHrv),
|
|
|
|
baselineHrv: baselineRmssd,
|
|
|
|
baselineAwakeHrv: baselineAwakeRmssd,
|
|
|
|
baselineSleepHrv: isSleep ? baselineSleepRmssd : null,
|
|
|
|
baselineRestingHr: baselineRestingHr,
|
|
|
|
flags: point.flags,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
validRmssdHistory.add(point);
|
|
|
|
}
|
|
|
|
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
List<HealthRawRealtimeStressPoint> _calculateRealtimeStressPoints({
|
|
|
|
required List<HealthRawMarkedDataPoint> heartRatePoints,
|
|
|
|
required List<HealthRawMarkedDataPoint> restingHeartRatePoints,
|
|
|
|
required List<HealthRawHrvStressPoint> hrvStressPoints,
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
}) {
|
|
|
|
final results = <HealthRawRealtimeStressPoint>[];
|
|
|
|
final days = _daysInRange(startTime, endTime);
|
|
|
|
double? previousAwakeStress;
|
|
|
|
int? previousAwakeStressTime;
|
|
|
|
double? previousSleepStress;
|
|
|
|
int? previousSleepStressTime;
|
|
|
|
double? awakeHrvAnchorStress;
|
|
|
|
double? sleepHrvAnchorStress;
|
|
|
|
|
|
|
|
for (final dayStart in days) {
|
|
|
|
final dayEnd = math.min(dayStart + Duration.secondsPerDay, endTime);
|
|
|
|
final dayHr = heartRatePoints
|
|
|
|
.where((e) => e.endTime >= dayStart && e.endTime <= dayEnd)
|
|
|
|
.toList();
|
|
|
|
if (dayHr.isEmpty) continue;
|
|
|
|
|
|
|
|
var pointTime = dayHr.first.endTime;
|
|
|
|
while (pointTime <= dayEnd) {
|
|
|
|
final hrWindow = _pointsInRange(
|
|
|
|
dayHr,
|
|
|
|
pointTime - 6 * 60 + 1,
|
|
|
|
pointTime,
|
|
|
|
);
|
|
|
|
final currentRawPoint = hrWindow.isEmpty ? null : hrWindow.last;
|
|
|
|
final context = currentRawPoint?.flags.context;
|
|
|
|
final contextWindow = context == null
|
|
|
|
? <HealthRawMarkedDataPoint>[]
|
|
|
|
: hrWindow.where((e) => e.flags.matchesContext(context)).toList();
|
|
|
|
final currentHr = _median(contextWindow.map((e) => e.value!).toList());
|
|
|
|
if (currentRawPoint != null && currentHr != null) {
|
|
|
|
final isSleep = context == HealthRawPointContext.sleep;
|
|
|
|
final isActivity = context == HealthRawPointContext.workout ||
|
|
|
|
context == HealthRawPointContext.workoutRecovery ||
|
|
|
|
context == HealthRawPointContext.suspectedActivity;
|
|
|
|
final awakeHistory = _latestBefore(
|
|
|
|
restingHeartRatePoints
|
|
|
|
.where((e) => e.flags.isAwakeHrBaselineEligible)
|
|
|
|
.toList(),
|
|
|
|
pointTime,
|
|
|
|
limit: baselineSampleCount,
|
|
|
|
);
|
|
|
|
final baselineAwakeHr = _baseline(
|
|
|
|
awakeHistory.map((e) => e.value!).toList(),
|
|
|
|
requiredCount: baselineSampleCount,
|
|
|
|
fallback: 65,
|
|
|
|
);
|
|
|
|
final sleepHistory = _latestBefore(
|
|
|
|
restingHeartRatePoints
|
|
|
|
.where((e) => e.flags.isSleepHrBaselineEligible)
|
|
|
|
.toList(),
|
|
|
|
pointTime,
|
|
|
|
limit: baselineSampleCount,
|
|
|
|
);
|
|
|
|
final baselineSleepHr = _sleepBaseline(
|
|
|
|
sleepHistory.map((e) => e.value!).toList(),
|
|
|
|
awakeHistoryCount: awakeHistory.length,
|
|
|
|
baselineAwakeHr: baselineAwakeHr,
|
|
|
|
);
|
|
|
|
final restingHistory = _latestBefore(
|
|
|
|
restingHeartRatePoints,
|
|
|
|
pointTime,
|
|
|
|
limit: baselineSampleCount,
|
|
|
|
);
|
|
|
|
final baselineRestingHr = isSleep ? baselineSleepHr : baselineAwakeHr;
|
|
|
|
final hrChange =
|
|
|
|
(currentHr - baselineRestingHr) / baselineRestingHr * 100;
|
|
|
|
final hrStress = _clamp(_hrStress(hrChange), 1, 100);
|
|
|
|
final smoothStress = isActivity
|
|
|
|
? hrStress
|
|
|
|
: _smoothedStress(
|
|
|
|
hrStress: hrStress,
|
|
|
|
pointTime: pointTime,
|
|
|
|
previousStress:
|
|
|
|
isSleep ? previousSleepStress : previousAwakeStress,
|
|
|
|
previousStressTime: isSleep
|
|
|
|
? previousSleepStressTime
|
|
|
|
: previousAwakeStressTime,
|
|
|
|
);
|
|
|
|
final latestHrv = isActivity
|
|
|
|
? null
|
|
|
|
: _lastHrvStressWhere(
|
|
|
|
hrvStressPoints,
|
|
|
|
(e) =>
|
|
|
|
e.rawEndTime <= pointTime && e.flags.context == context,
|
|
|
|
);
|
|
|
|
final adjustment = _adjustedStress(
|
|
|
|
smoothStress: smoothStress,
|
|
|
|
pointTime: pointTime,
|
|
|
|
latestHrv: latestHrv,
|
|
|
|
hrvAnchorStress:
|
|
|
|
isSleep ? sleepHrvAnchorStress : awakeHrvAnchorStress,
|
|
|
|
);
|
|
|
|
if (adjustment.updatedAnchorStress != null) {
|
|
|
|
if (isSleep) {
|
|
|
|
sleepHrvAnchorStress = adjustment.updatedAnchorStress;
|
|
|
|
} else {
|
|
|
|
awakeHrvAnchorStress = adjustment.updatedAnchorStress;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
final stress = _integerDouble(_clamp(adjustment.value, 1, 100));
|
|
|
|
final sourceRange = _sourceRangeForRealtime(
|
|
|
|
hrWindow: hrWindow,
|
|
|
|
restingHistory: restingHistory,
|
|
|
|
latestHrv: latestHrv,
|
|
|
|
);
|
|
|
|
results.add(
|
|
|
|
HealthRawRealtimeStressPoint(
|
|
|
|
userId: userId,
|
|
|
|
rawEndTime: currentRawPoint.endTime,
|
|
|
|
rawHr: currentRawPoint.value ?? 0,
|
|
|
|
result: stress,
|
|
|
|
sourceStartTime: sourceRange.start,
|
|
|
|
sourceEndTime: sourceRange.end,
|
|
|
|
flags: currentRawPoint.flags,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
if (!isActivity) {
|
|
|
|
if (isSleep) {
|
|
|
|
previousSleepStress = stress;
|
|
|
|
previousSleepStressTime = pointTime;
|
|
|
|
} else {
|
|
|
|
previousAwakeStress = stress;
|
|
|
|
previousAwakeStressTime = pointTime;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pointTime += 6 * 60;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
static ({int start, int end}) _sourceRangeForRealtime({
|
|
|
|
required List<HealthRawMarkedDataPoint> hrWindow,
|
|
|
|
required List<HealthRawMarkedDataPoint> restingHistory,
|
|
|
|
required HealthRawHrvStressPoint? latestHrv,
|
|
|
|
}) {
|
|
|
|
final pointRanges = _timeRange([...hrWindow, ...restingHistory]);
|
|
|
|
if (latestHrv == null) return pointRanges;
|
|
|
|
return (
|
|
|
|
start: math.min(pointRanges.start, latestHrv.sourceStartTime),
|
|
|
|
end: math.max(pointRanges.end, latestHrv.sourceEndTime),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static List<T> _latestBefore<T extends HealthRawMarkedDataPoint>(
|
|
|
|
List<T> points,
|
|
|
|
int time, {
|
|
|
|
required int limit,
|
|
|
|
}) {
|
|
|
|
final history = points.where((e) => e.endTime < time).toList();
|
|
|
|
if (history.length <= limit) return history;
|
|
|
|
return history.sublist(history.length - limit);
|
|
|
|
}
|
|
|
|
|
|
|
|
static List<T> _pointsInRange<T extends HealthRawMarkedDataPoint>(
|
|
|
|
List<T> points,
|
|
|
|
int start,
|
|
|
|
int end,
|
|
|
|
) {
|
|
|
|
return points.where((e) => e.endTime >= start && e.endTime <= end).toList();
|
|
|
|
}
|
|
|
|
|
|
|
|
static ({int start, int end}) _timeRange(
|
|
|
|
List<HealthRawMarkedDataPoint> points) {
|
|
|
|
if (points.isEmpty) return (start: 0, end: 0);
|
|
|
|
var start = points.first.startTime;
|
|
|
|
var end = points.first.endTime;
|
|
|
|
for (final point in points.skip(1)) {
|
|
|
|
start = math.min(start, point.startTime);
|
|
|
|
end = math.max(end, point.endTime);
|
|
|
|
}
|
|
|
|
return (start: start, end: end);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool _isValidRmssd(double? value) {
|
|
|
|
return value != null && value >= rawRmssdMin && value <= rawRmssdMax;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool _isValidHr(double value) => value >= 30 && value <= 220;
|
|
|
|
|
|
|
|
static double _baseline(
|
|
|
|
List<double> values, {
|
|
|
|
required int requiredCount,
|
|
|
|
required double fallback,
|
|
|
|
}) {
|
|
|
|
if (values.length < requiredCount) return fallback;
|
|
|
|
final suffix = values.sublist(values.length - requiredCount);
|
|
|
|
return _median(suffix) ?? fallback;
|
|
|
|
}
|
|
|
|
|
|
|
|
static double _sleepBaseline(
|
|
|
|
List<double> sleepValues, {
|
|
|
|
required int awakeHistoryCount,
|
|
|
|
required double baselineAwakeHr,
|
|
|
|
}) {
|
|
|
|
if (sleepValues.length >= baselineSampleCount) {
|
|
|
|
return _median(sleepValues.sublist(
|
|
|
|
sleepValues.length - baselineSampleCount,
|
|
|
|
)) ??
|
|
|
|
65;
|
|
|
|
}
|
|
|
|
if (sleepValues.length >= 10) {
|
|
|
|
return _median(sleepValues) ?? 65;
|
|
|
|
}
|
|
|
|
return awakeHistoryCount >= baselineSampleCount ? baselineAwakeHr : 65;
|
|
|
|
}
|
|
|
|
|
|
|
|
static double? _median(List<double> values) {
|
|
|
|
if (values.isEmpty) return null;
|
|
|
|
final sorted = [...values]..sort();
|
|
|
|
final middle = sorted.length ~/ 2;
|
|
|
|
if (sorted.length.isEven) {
|
|
|
|
return (sorted[middle - 1] + sorted[middle]) / 2;
|
|
|
|
}
|
|
|
|
return sorted[middle];
|
|
|
|
}
|
|
|
|
|
|
|
|
static double _hrStress(double hrChange) {
|
|
|
|
if (hrChange <= -20) return 5;
|
|
|
|
if (hrChange <= -10) return 5 + (hrChange + 20) * 0.9;
|
|
|
|
if (hrChange <= 0) return 14 + (hrChange + 10) * 1.4;
|
|
|
|
if (hrChange <= 10) return 28 + hrChange * 1.6;
|
|
|
|
if (hrChange <= 20) return 44 + (hrChange - 10) * 1.6;
|
|
|
|
if (hrChange <= 30) return 60 + (hrChange - 20) * 1.5;
|
|
|
|
if (hrChange <= 40) return 75 + (hrChange - 30) * 1.3;
|
|
|
|
if (hrChange < 55) return 88 + (hrChange - 40) * 0.8;
|
|
|
|
return 100;
|
|
|
|
}
|
|
|
|
|
|
|
|
static double _smoothedStress({
|
|
|
|
required double hrStress,
|
|
|
|
required int pointTime,
|
|
|
|
required double? previousStress,
|
|
|
|
required int? previousStressTime,
|
|
|
|
}) {
|
|
|
|
if (previousStress == null ||
|
|
|
|
previousStressTime == null ||
|
|
|
|
pointTime - previousStressTime > 12 * 60) {
|
|
|
|
return hrStress;
|
|
|
|
}
|
|
|
|
|
|
|
|
final stressDiff = (hrStress - previousStress).abs();
|
|
|
|
if (stressDiff < 30) {
|
|
|
|
return hrStress * 0.60 + previousStress * 0.40;
|
|
|
|
}
|
|
|
|
return hrStress * 0.80 + previousStress * 0.20;
|
|
|
|
}
|
|
|
|
|
|
|
|
static ({double value, double? updatedAnchorStress}) _adjustedStress({
|
|
|
|
required double smoothStress,
|
|
|
|
required int pointTime,
|
|
|
|
required HealthRawHrvStressPoint? latestHrv,
|
|
|
|
required double? hrvAnchorStress,
|
|
|
|
}) {
|
|
|
|
if (latestHrv == null) {
|
|
|
|
return (value: smoothStress, updatedAnchorStress: null);
|
|
|
|
}
|
|
|
|
|
|
|
|
final hrvAge = (pointTime - latestHrv.rawEndTime) / 60;
|
|
|
|
if (hrvAge >= 0 && hrvAge <= 6) {
|
|
|
|
final adjusted = switch (latestHrv.state) {
|
|
|
|
HealthRawStressState.excellent => _clamp(smoothStress, 1, 20.9),
|
|
|
|
HealthRawStressState.normal => _clamp(smoothStress, 21, 60.9),
|
|
|
|
HealthRawStressState.attention => _clamp(smoothStress, 61, 80.9),
|
|
|
|
HealthRawStressState.overload => _clamp(smoothStress, 81, 100),
|
|
|
|
};
|
|
|
|
return (value: adjusted, updatedAnchorStress: adjusted);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hrvAge > 6 && hrvAge <= 30) {
|
|
|
|
return switch (latestHrv.state) {
|
|
|
|
HealthRawStressState.excellent => (
|
|
|
|
value: math.min(smoothStress, 60.9).toDouble(),
|
|
|
|
updatedAnchorStress: null,
|
|
|
|
),
|
|
|
|
HealthRawStressState.normal => (
|
|
|
|
value: math.min(smoothStress, 80.9).toDouble(),
|
|
|
|
updatedAnchorStress: null,
|
|
|
|
),
|
|
|
|
HealthRawStressState.attention => (
|
|
|
|
value: math.max(smoothStress, 21).toDouble(),
|
|
|
|
updatedAnchorStress: null,
|
|
|
|
),
|
|
|
|
HealthRawStressState.overload => (
|
|
|
|
value: math.max(smoothStress, 61).toDouble(),
|
|
|
|
updatedAnchorStress: null,
|
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hrvAge > 30 && hrvAge <= 60 && hrvAnchorStress != null) {
|
|
|
|
final hrvWeight = (60 - hrvAge) / 30 * 0.20;
|
|
|
|
return (
|
|
|
|
value: smoothStress * (1 - hrvWeight) + hrvAnchorStress * hrvWeight,
|
|
|
|
updatedAnchorStress: null,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (value: smoothStress, updatedAnchorStress: null);
|
|
|
|
}
|
|
|
|
|
|
|
|
static HealthRawStressState _hrvState(double value) {
|
|
|
|
if (value >= 30) return HealthRawStressState.excellent;
|
|
|
|
if (value >= 21) return HealthRawStressState.normal;
|
|
|
|
if (value >= 17) return HealthRawStressState.attention;
|
|
|
|
return HealthRawStressState.overload;
|
|
|
|
}
|
|
|
|
|
|
|
|
static double _clamp(double value, double lower, double upper) {
|
|
|
|
return math.min(math.max(value, lower), upper).toDouble();
|
|
|
|
}
|
|
|
|
|
|
|
|
static double _integerDouble(num value) => value.toInt().toDouble();
|
|
|
|
|
|
|
|
static List<int> _daysInRange(int startTime, int endTime) {
|
|
|
|
final start = DateTime.fromMillisecondsSinceEpoch(startTime * 1000);
|
|
|
|
final end = DateTime.fromMillisecondsSinceEpoch(endTime * 1000);
|
|
|
|
var day = DateTime(start.year, start.month, start.day);
|
|
|
|
final finalDay = DateTime(end.year, end.month, end.day);
|
|
|
|
final days = <int>[];
|
|
|
|
while (!day.isAfter(finalDay)) {
|
|
|
|
days.add(day.millisecondsSinceEpoch ~/ 1000);
|
|
|
|
day = day.add(const Duration(days: 1));
|
|
|
|
}
|
|
|
|
return days;
|
|
|
|
}
|
|
|
|
|
|
|
|
static HealthRawHrvStressPoint? _lastHrvStressWhere(
|
|
|
|
Iterable<HealthRawHrvStressPoint> points,
|
|
|
|
bool Function(HealthRawHrvStressPoint point) test,
|
|
|
|
) {
|
|
|
|
HealthRawHrvStressPoint? result;
|
|
|
|
for (final point in points) {
|
|
|
|
if (test(point)) result = point;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int _dateKeyFromUnixSeconds(int seconds) {
|
|
|
|
final dateTime = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
|
|
|
|
return dateTime.year * 10000 + dateTime.month * 100 + dateTime.day;
|
|
|
|
}
|
|
|
|
|
|
|
|
static (int, int) _dayRangeFromDateKey(int date) {
|
|
|
|
final year = date ~/ 10000;
|
|
|
|
final month = (date ~/ 100) % 100;
|
|
|
|
final day = date % 100;
|
|
|
|
final start = DateTime(year, month, day).millisecondsSinceEpoch ~/ 1000;
|
|
|
|
return (start, start + Duration.secondsPerDay - 1);
|
|
|
|
}
|
|
|
|
} |
...
|
...
|
|