Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson
2025-01-27 11:38:16 -08:00
committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions
@@ -0,0 +1,479 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <stdint.h>
#include <string.h>
#include "services/normal/activity/activity_algorithm.h"
#include "util/size.h"
// ----------------------------------------------------------------
// Sample captured at: 2015-08-18 22:15:00 local, 2015-08-19 05:15:00 GMT
// According to Misfit:
// Total sleep from 10:42 pm to 4:06am (325 minutes)
// Deep sleep 182 min.
// Breakdown (very approximate):
// 10:15 - 10:42 awake (27 min)
// 10:42 - 10:50 light (8 min)
// 10:50 - 11:20 deep (30 min)
// 11:20 - 11:30 light (10 min)
// 11:30 - 11:40 deep (10 min)
// 11:40 - 12:30 light (50 min)
// 12:30 - 2:45 deep (135 min)
// 2:45 - 3:15 light (30 min)
// 3:15 - 3:35 deep (20 min)
// 3:35 - 4:06 light (30 min)
// 4:06 - 5:00 awake
AlgMinuteFileSampleV5 *activity_sample_sleep_v1_1(int *len) {
// list of: {steps, orientation, vmc}
static AlgMinuteFileSampleV5 samples[] = {
{0, 0x8f, 6},
{0, 0x8f, 0},
{0, 0x8f, 0},
{0, 0x8f, 0},
{0, 0x8f, 0},
{0, 0x8f, 0},
{0, 0x8f, 4},
{0, 0x8f, 0},
{0, 0x8f, 0},
{0, 0x65, 47},
{0, 0x44, 5},
{8, 0x63, 36},
{10, 0x52, 68},
{8, 0x53, 61},
{0, 0x63, 83},
// 15: Local time: 10:30:00 PM
{0, 0x64, 107},
{0, 0x79, 35},
{0, 0x78, 0},
{0, 0x78, 37},
{0, 0x68, 4},
{0, 0x68, 0},
{0, 0x68, 0},
{0, 0x69, 17},
{0, 0x69, 0},
{0, 0x68, 10},
{0, 0x67, 0},
{0, 0x68, 12},
{0, 0x68, 0}, // start light
{0, 0x68, 0},
{0, 0x67, 41},
// 30: Local time: 10:45:00 PM
{0, 0x64, 26},
{0, 0x54, 0},
{0, 0x54, 12},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0}, // start deep (30 min)
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x54, 12},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
// 45: Local time: 11:00:00 PM
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
// 60: Local time: 11:15:00 PM
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0}, // start light
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 19},
{0, 0x54, 0},
{0, 0x54, 6},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
// 75: Local time: 11:30:00 PM
{0, 0x64, 0}, // start deep
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0}, // start light
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x54, 16},
{0, 0x64, 0},
{0, 0x64, 0},
// 90: Local time: 11:45:00 PM
{0, 0x64, 0},
{0, 0x54, 2},
{0, 0x54, 0},
{0, 0x54, 7},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 14},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x64, 16},
{0, 0x64, 0},
{0, 0x54, 13},
{0, 0x44, 0},
// 105: Local time: 12:00:00 AM
{0, 0x44, 0},
{0, 0x44, 0},
{0, 0x44, 8},
{0, 0x44, 0},
{0, 0x44, 0},
{0, 0x44, 0},
{0, 0x44, 0},
{0, 0x44, 0},
{0, 0x44, 0},
{0, 0x44, 0},
{0, 0x44, 6},
{0, 0x44, 0},
{0, 0x44, 0},
{0, 0x44, 0},
{0, 0x44, 0},
// 120: Local time: 12:15:00 AM
{0, 0x44, 2},
{0, 0x44, 8},
{0, 0x44, 0},
{0, 0x44, 12},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x54, 0},
{0, 0x64, 28},
{0, 0x65, 28},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
// 135: Local time: 12:30:00 AM
{0, 0x16, 0}, // start deep
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
// 150: Local time: 12:45:00 AM
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 18},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
// 165: Local time: 01:00:00 AM
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
// 180: Local time: 01:15:00 AM
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x16, 0},
{0, 0x36, 14},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
// 195: Local time: 01:30:00 AM
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
// 210: Local time: 01:45:00 AM
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
// 225: Local time: 02:00:00 AM
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
// 240: Local time: 02:15:00 AM
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
// 255: Local time: 02:30:00 AM
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x55, 18},
{0, 0x15, 0},
{0, 0x15, 0},
{0, 0x15, 0},
{0, 0x15, 0},
{0, 0x15, 5},
// 270: Local time: 02:45:00 AM
{0, 0x26, 26}, // start light
{0, 0x26, 0},
{0, 0x26, 0},
{0, 0x26, 0},
{0, 0x26, 0},
{0, 0x26, 0},
{0, 0x26, 0},
{0, 0x26, 0},
{0, 0x26, 0},
{0, 0x26, 0},
{0, 0x26, 0},
{0, 0x26, 0},
{0, 0x35, 20},
{0, 0x55, 0},
{0, 0x55, 0},
// 285: Local time: 03:00:00 AM
{0, 0x55, 0},
{0, 0x55, 0},
{0, 0x55, 0},
{0, 0x55, 0},
{0, 0x55, 0},
{0, 0x65, 9},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
// 300: Local time: 03:15:00 AM
{0, 0x65, 0}, // start deep
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
// 315: Local time: 03:30:00 AM
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0}, // start light
{0, 0x65, 0},
{0, 0x66, 17},
{0, 0x2a, 0},
{0, 0x2a, 0},
{0, 0x2a, 0},
{0, 0x2a, 0},
{0, 0x2b, 0},
{0, 0x2a, 0},
{0, 0x2a, 0},
{0, 0x2a, 0},
// 330: Local time: 03:45:00 AM
{0, 0x2a, 0},
{0, 0x57, 19},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x65, 0},
{0, 0x66, 20},
{0, 0x77, 8},
{0, 0x66, 6},
{0, 0x67, 18},
{0, 0x68, 0},
{0, 0x68, 11},
{0, 0x23, 44},
{0, 0x42, 62},
{0, 0x64, 0},
{0, 0x64, 0},
// 345: Local time: 04:00:00 AM
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0}, // wake up
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 0},
{0, 0x64, 7},
{0, 0x63, 29},
{0, 0x64, 0},
{0, 0x7f, 37},
{0, 0x82, 21},
{0, 0x75, 17},
// 360: Local time: 04:15:00 AM
{12, 0x43, 63},
{31, 0x32, 55},
{0, 0x63, 40},
{0, 0x71, 42},
{0, 0x7f, 0},
{0, 0x7f, 0},
{0, 0x7f, 0},
{0, 0x7f, 0},
{0, 0x7f, 0},
{0, 0x7f, 0},
{0, 0x7f, 0},
{0, 0x70, 42},
{0, 0x65, 20},
{0, 0x77, 17},
{0, 0x77, 26},
// 375: Local time: 04:30:00 AM
{0, 0x77, 18},
{0, 0x67, 22},
{0, 0x65, 30},
{0, 0x68, 36},
{0, 0x68, 14},
{0, 0x68, 18},
{0, 0x76, 30},
{0, 0x75, 30},
{0, 0x65, 31},
{0, 0x65, 38},
{9, 0x70, 48},
{0, 0x71, 0},
{0, 0x6f, 30},
{0, 0x6f, 4},
{0, 0x6f, 0},
// 390: Local time: 04:45:00 AM
{0, 0x6f, 0},
{0, 0x6f, 0},
{0, 0x6f, 0},
{0, 0x6f, 0},
{0, 0x6f, 0},
{0, 0x6f, 0},
{0, 0x6f, 2},
{0, 0x6f, 0},
{0, 0x6f, 0},
{0, 0x6f, 0},
{0, 0x64, 48},
{0, 0x55, 0},
{0, 0x55, 36},
{0, 0x67, 32},
{0, 0x55, 0},
};
*len = ARRAY_LENGTH(samples);
return samples;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,960 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/accel_service.h"
#include "applib/data_logging.h"
#include "drivers/ambient_light.h"
#include "drivers/rtc.h"
#include "services/common/regular_timer.h"
#include "services/common/battery/battery_state.h"
#include "services/normal/activity/activity.h"
#include "services/normal/activity/activity_private.h"
#include "services/normal/activity/activity_algorithm.h"
#include "services/normal/activity/kraepelin/activity_algorithm_kraepelin.h"
#include "services/normal/activity/kraepelin/kraepelin_algorithm.h"
#include "services/normal/data_logging/data_logging_service.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/settings/settings_file.h"
#include "services/common/system_task.h"
#include "util/math.h"
#include "util/size.h"
#include <stdint.h>
#include <string.h>
#include <applib/health_service.h>
#include <services/normal/activity/kraepelin/activity_algorithm_kraepelin.h>
#include "clar.h"
// Stubs
#include "stubs_analytics.h"
#include "stubs_freertos.h"
#include "stubs_hexdump.h"
#include "stubs_hr_util.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_prompt.h"
#include "stubs_sleep.h"
#include "stubs_task_watchdog.h"
// Fakes
#include "fake_accel_service.h"
#include "fake_new_timer.h"
#include "fake_rtc.h"
#include "fake_spi_flash.h"
#include "fake_system_task.h"
#define ASSERT_EQUAL_I(i1,i2,file,line) \
clar__assert_equal_i((i1),(i2),file,line,#i1 " != " #i2, 1)
// Globals
AccelSamplingRate s_sample_rate;
static bool s_dls_created;
static DataLoggingSession *s_dls_session = (DataLoggingSession *)1;
// Logged items
static bool s_capture_dls_records = true;
static int s_num_dls_records;
static AlgMinuteDLSRecord s_dls_records[100];
// Which step count to return from kalg_analyze_samples()
static uint16_t s_alg_next_steps;
// Which vmc and orientation to return from kalg_minute_stats
static uint16_t s_alg_next_vmc;
static uint8_t s_alg_next_orientation;
static uint8_t s_alg_next_light;
static bool s_alg_next_plugged_in;
static struct tm s_start_time_tm = {
.tm_hour = 17,
.tm_mday = 1,
.tm_mon = 0,
.tm_year = 115
};
// ============================================================================================
// Misc stubs
uint32_t ambient_light_get_light_level(void) {
return s_alg_next_light << 4;
}
AmbientLightLevel ambient_light_level_to_enum(uint32_t light_level) {
// Just return a predictable result to validate the unit tests
return (light_level / ALG_RAW_LIGHT_SENSOR_DIVIDE_BY) % AMBIENT_LIGHT_LEVEL_ENUM_COUNT;
}
BatteryChargeState battery_get_charge_state(void) {
BatteryChargeState state = {
.charge_percent = 50,
.is_charging = s_alg_next_plugged_in,
.is_plugged = s_alg_next_plugged_in,
};
return state;
}
void kalg_enable_activity_tracking(KAlgState *kalg_state, bool enable) {}
bool activity_tracking_on(void) {
return true;
}
// ------------------------------------------------------------------------------------
// Return true if the given activity type is a sleep activity
bool activity_sessions_prv_is_sleep_activity(ActivitySessionType activity_type) {
switch (activity_type) {
case ActivitySessionType_Sleep:
case ActivitySessionType_RestfulSleep:
case ActivitySessionType_Nap:
case ActivitySessionType_RestfulNap:
return true;
case ActivitySessionType_Walk:
case ActivitySessionType_Run:
case ActivitySessionType_Open:
return false;
case ActivitySessionType_None:
case ActivitySessionTypeCount:
break;
}
WTF;
}
// ------------------------------------------------------------------------------------
uint16_t s_activity_sessions_count;
ActivitySession s_activity_sessions[ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT];
void activity_sessions_prv_add_activity_session(ActivitySession *session) {
// If this is a duplicate activity, ignore it
ActivitySession *stored_session = s_activity_sessions;
for (uint16_t i = 0; i < s_activity_sessions_count; i++, stored_session++) {
if ((session->type == stored_session->type)
&& (session->start_utc == stored_session->start_utc)) {
return;
}
}
// If no more room, fail
if (s_activity_sessions_count >= ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT) {
PBL_LOG(LOG_LEVEL_WARNING, "No more room for additional activities");
return;
}
// Add this activity in
s_activity_sessions[s_activity_sessions_count++] = *session;
}
// ------------------------------------------------------------------------------------
void activity_sessions_prv_delete_activity_session(ActivitySession *session) {
}
// =============================================================================================
// Data logging stubs
DataLoggingResult dls_log(DataLoggingSession *logging_session, const void *data,
uint32_t num_items) {
if (!s_capture_dls_records) {
return DATA_LOGGING_SUCCESS;
}
cl_assert(s_dls_created);
cl_assert_equal_p(logging_session, s_dls_session);
AlgMinuteDLSRecord *records = (AlgMinuteDLSRecord *)data;
for (int i = 0; i < num_items; i++) {
cl_assert(s_num_dls_records < ARRAY_LENGTH(s_dls_records));
s_dls_records[s_num_dls_records++] = records[i];
}
return DATA_LOGGING_SUCCESS;
}
DataLoggingSession *dls_create(uint32_t tag, DataLoggingItemType item_type, uint16_t item_size,
bool buffered, bool resume, const Uuid *uuid) {
s_dls_created = true;
cl_assert_equal_i(item_size, sizeof(AlgMinuteDLSRecord));
return s_dls_session;
}
void dls_send_all_sessions(void) {
}
// ============================================================================================
// Activity service stubs
// --------------------------------------------------------------------------------------------
// Values to return from activity_private_get_.*()
static uint32_t s_activity_next_distance_mm;
static uint32_t s_activity_next_active_calories;
static uint32_t s_activity_next_resting_calories;
static uint32_t s_activity_next_heart_rate_bpm;
static uint32_t s_activity_next_heart_rate_zone;
static uint32_t s_activity_next_heart_rate_heart_rate_total_weight_x100;
uint32_t activity_metrics_prv_get_steps(void) {
return 0;
}
uint32_t activity_metrics_prv_get_distance_mm(void) {
return s_activity_next_distance_mm;
}
// --------------------------------------------------------------------------------------------
uint32_t activity_metrics_prv_get_resting_calories(void) {
return s_activity_next_resting_calories;
}
// --------------------------------------------------------------------------------------------
uint32_t activity_metrics_prv_get_active_calories(void) {
return s_activity_next_active_calories;
}
HRZone activity_metrics_prv_get_hr_zone(void) {
return s_activity_next_heart_rate_zone;
}
void activity_metrics_prv_get_median_hr_bpm(int32_t *median, int32_t *total_weight) {
if (median) {
*median = s_activity_next_heart_rate_bpm;
}
if (total_weight) {
*total_weight = s_activity_next_heart_rate_heart_rate_total_weight_x100;
}
}
void activity_metrics_prv_reset_hr_stats(void) {
s_activity_next_heart_rate_bpm = 0;
s_activity_next_heart_rate_zone = 0;
}
// =============================================================================================
// Algorithm stubs
uint32_t kalg_state_size(void) {
return 1;
}
bool kalg_init(KAlgState *state, KAlgStatsCallback stats_cb) {
return true;
}
uint32_t kalg_analyze_samples(KAlgState *state, AccelRawData *data, uint32_t num_samples,
uint32_t *consumed_samples) {
*consumed_samples = 0;
return s_alg_next_steps;
}
void kalg_minute_stats(KAlgState *state, uint16_t *vmc, uint8_t *orientation, bool *still) {
*vmc = s_alg_next_vmc;
*orientation = s_alg_next_orientation;
*still = false;
}
void kalg_set_weight(KAlgState *state, uint32_t grams) {
}
void kalg_activities_update(KAlgState *state, time_t utc_now, uint16_t steps, uint16_t vmc,
uint8_t orientation, bool plugged_in, uint32_t resting_calories,
uint32_t active_calories, uint32_t distance_mm, bool shutting_down,
KAlgActivitySessionCallback sessions_cb, void *context) {
}
time_t kalg_activity_last_processed_time(KAlgState *state, KAlgActivityType activity) {
return rtc_get_time();
}
// Set these to simulate a sleep session (that should result in zeroing out any steps taken)
static time_t s_kalg_sleep_start_utc;
static uint16_t s_kalg_sleep_m;
void kalg_get_sleep_stats(KAlgState *alg_state, KAlgOngoingSleepStats *stats) {
time_t now = rtc_get_time();
if (s_kalg_sleep_start_utc == 0 || now < s_kalg_sleep_start_utc + SECONDS_PER_HOUR) {
// We are before the requested sleep time
*stats = (KAlgOngoingSleepStats) { };
} else {
// We are somewhere after the start of sleep
time_t sleep_end = s_kalg_sleep_start_utc + s_kalg_sleep_m * SECONDS_PER_MINUTE;
if (now < sleep_end + KALG_MAX_UNCERTAIN_SLEEP_M) {
// Still haven't detected the end of sleep, the last KALG_MAX_UNCERTAIN_SLEEP_M minutes are
// uncertain
*stats = (KAlgOngoingSleepStats) {
.sleep_start_utc = s_kalg_sleep_start_utc,
.sleep_len_m = (now - s_kalg_sleep_start_utc) / SECONDS_PER_MINUTE
- KALG_MAX_UNCERTAIN_SLEEP_M,
.uncertain_start_utc = now - KALG_MAX_UNCERTAIN_SLEEP_M * SECONDS_PER_MINUTE,
};
} else {
// The sleep was in the past and has ended
*stats = (KAlgOngoingSleepStats) {
.sleep_start_utc = s_kalg_sleep_start_utc,
.sleep_len_m = s_kalg_sleep_m,
.uncertain_start_utc = 0,
};
}
}
}
// --------------------------------------------------------------------------------------------
// Create sample data for testing sleep and data logging
static void prv_create_test_data(uint32_t num_minutes, AlgMinuteDLSSample *minute_data) {
uint16_t next_vmc = 0;
uint8_t next_orient = 1;
uint8_t next_light = 2;
uint16_t next_active_calories = 3;
uint16_t next_resting_calories = 4;
uint16_t next_distance_cm = 5;
uint8_t next_heart_rate_bpm = 6;
uint8_t next_heart_rate_heart_rate_total_weight_x100 = 7;
uint8_t next_heart_rate_zone = 8;
bool next_plugged_in = false;
for (int i = 0; i < num_minutes; i++) {
minute_data[i] = (AlgMinuteDLSSample) { };
minute_data[i].base.steps = i;
minute_data[i].base.vmc = next_vmc++;
if (next_vmc == 65533) {
// Make sure combinations of vmc/orient are mostly unique, so don't wrap at the same
// module 256 boundary.
next_vmc = 0;
}
minute_data[i].base.orientation = next_orient++;
minute_data[i].base.light = next_light++;
minute_data[i].base.plugged_in = next_plugged_in;
next_plugged_in = !next_plugged_in;
minute_data[i].active_calories = next_active_calories++;
minute_data[i].resting_calories = next_resting_calories++;
minute_data[i].distance_cm = next_distance_cm++;
minute_data[i].base.active = (minute_data[i].base.steps >= ACTIVITY_ACTIVE_MINUTE_MIN_STEPS)
? 1 : 0;
minute_data[i].heart_rate_bpm = next_heart_rate_bpm++;
minute_data[i].heart_rate_total_weight_x100 = next_heart_rate_heart_rate_total_weight_x100++;
minute_data[i].heart_rate_zone = next_heart_rate_zone++;
}
}
// --------------------------------------------------------------------------------------------
// Feed in sleep data
static void prv_feed_minute_data(uint32_t num_minutes, AlgMinuteDLSSample *minute_data,
bool simulate_bg_delays) {
// Call the minute handler, which computes the minute stats and saves them to data logging
// as well as the sleep PFS file.
for (int i = 0; i < num_minutes; i++) {
fake_rtc_increment_time(SECONDS_PER_MINUTE);
s_alg_next_steps = minute_data[i].base.steps;
AccelRawData samples[100] = { };
uint64_t timestamp = 0;
// Calling activity_algorithm_handle_accel() on our stub algorithm gives it the step
// counts for this minute
activity_algorithm_handle_accel(samples, s_sample_rate, timestamp);
// Are we simulating delays in KernelBG processing?
int delay = 0;
if (simulate_bg_delays) {
delay = (((i / ALG_MINUTES_PER_FILE_RECORD) + 1) % 30);
rtc_set_time(rtc_get_time() + delay);
}
s_alg_next_vmc = minute_data[i].base.vmc;
s_alg_next_orientation = minute_data[i].base.orientation;
s_alg_next_light = minute_data[i].base.light;
s_alg_next_plugged_in = minute_data[i].base.plugged_in;
s_activity_next_distance_mm += minute_data[i].distance_cm * 10;
s_activity_next_resting_calories += minute_data[i].resting_calories;
s_activity_next_active_calories += minute_data[i].active_calories;
s_activity_next_heart_rate_bpm = minute_data[i].heart_rate_bpm;
s_activity_next_heart_rate_heart_rate_total_weight_x100 = minute_data[i].heart_rate_total_weight_x100;
s_activity_next_heart_rate_zone = minute_data[i].heart_rate_zone;
AlgMinuteRecord minute_record = {};
activity_algorithm_minute_handler(rtc_get_time(), &minute_record);
if (simulate_bg_delays) {
rtc_set_time(rtc_get_time() - delay);
}
}
}
// =============================================================================================
// Start of unit tests
void test_activity_algorithm_kraepelin__initialize(void) {
time_t utc_sec = mktime(&s_start_time_tm);
fake_rtc_init(100 /*initial_ticks*/, utc_sec);
fake_spi_flash_init(0, 0x1000000);
pfs_init(false);
pfs_format(false);
// Init the algorithm
s_activity_next_resting_calories = 0;
s_activity_next_distance_mm = 0;
s_activity_next_active_calories = 0;
s_activity_next_heart_rate_bpm = 0;
s_activity_next_heart_rate_zone = 0;
s_kalg_sleep_start_utc = 0;
s_kalg_sleep_m = 0;
activity_algorithm_init(&s_sample_rate);
}
// ---------------------------------------------------------------------------------------
void test_activity_algorithm_kraepelin__cleanup(void) {
fake_system_task_callbacks_invoke_pending();
activity_algorithm_deinit();
}
// ---------------------------------------------------------------------------------------
// Test to make sure that the minute data gets sent to data logging correctly
void test_activity_algorithm_kraepelin__data_logging_test(void) {
const int k_num_records = 2;
const int num_minutes = k_num_records * ALG_MINUTES_PER_DLS_RECORD;
// The test data
AlgMinuteDLSSample minute_data[num_minutes];
prv_create_test_data(num_minutes, minute_data);
// Call the minute handler, which computes the minute stats and saves them to data logging
// as well as the minute data settings file.
prv_feed_minute_data(num_minutes, minute_data, false /*simulate_bg_delays*/);
// Make sure the correct data got saved to data logging
cl_assert_equal_i(s_num_dls_records, 2);
for (int j = 0; j < k_num_records; j++) {
cl_assert_equal_i(s_dls_records[j].hdr.version, ALG_DLS_MINUTES_RECORD_VERSION);
for (int i = 0; i < ALG_MINUTES_PER_DLS_RECORD; i++) {
cl_assert_equal_m(&s_dls_records[j].samples[i],
&minute_data[(j * ALG_MINUTES_PER_DLS_RECORD) + i],
sizeof(minute_data[i]));
}
}
}
// ------------------------------------------------------------------------------------
static void prv_assert_minute_data(HealthMinuteData *actual, AlgMinuteDLSSample *expected) {
cl_assert_equal_i(actual->steps, expected->base.steps);
cl_assert_equal_i(actual->orientation, expected->base.orientation);
cl_assert_equal_i(actual->vmc, expected->base.vmc);
cl_assert_equal_i(actual->light, ambient_light_level_to_enum(
expected->base.light * ALG_RAW_LIGHT_SENSOR_DIVIDE_BY));
cl_assert_equal_i(actual->heart_rate_bpm, expected->heart_rate_bpm);
}
// ---------------------------------------------------------------------------------------
// Test to make sure that when we re-boot we correctly get the saved minute data
void test_activity_algorithm_kraepelin__minute_data_after_boot(void) {
const int num_minutes = 4 * MINUTES_PER_HOUR;
time_t start_utc = rtc_get_time();
// The test data
AlgMinuteDLSSample minute_data[num_minutes];
prv_create_test_data(num_minutes, minute_data);
// Write first half of the data
prv_feed_minute_data(num_minutes / 2, minute_data, false /*simulate_bg_delays*/);
// Now, simulate a reboot, re-initialize of the algorithm. This will trigger a re-read
// of the sleep data file
activity_algorithm_deinit();
activity_algorithm_init(&s_sample_rate);
// Write the rest of the data
int start_idx = num_minutes / 2;
prv_feed_minute_data(num_minutes / 2, minute_data + start_idx, false /*simulate_bg_delays*/);
// Retrieve all the minute data and verify the contents
HealthMinuteData retrieve[num_minutes];
uint32_t num_records = num_minutes;
time_t start = start_utc;
activity_algorithm_get_minute_history(retrieve, &num_records, &start);
cl_assert_equal_i(num_records, num_minutes);
cl_assert_equal_i(start, start_utc);
for (int i = 0; i < num_minutes; i++) {
prv_assert_minute_data(&retrieve[i], &minute_data[i]);
}
}
// ---------------------------------------------------------------------------------------
// Test to make sure that the minute data file gets compacted correctly. If we write more than
// ALG_MINUTE_DATA_FILE_LEN worth of data to the sleep file, it's size should be capped at
// ALG_MINUTE_DATA_FILE_LEN and we should be able to successfully read back the most recent
// data we wrote.
void test_activity_algorithm_kraepelin__sleep_data_compaction_test(void) {
const int num_minutes = ALG_SLEEP_HISTORY_HOURS_FOR_TODAY * MINUTES_PER_HOUR;
// The test data
AlgMinuteDLSSample minute_data[num_minutes];
prv_create_test_data(num_minutes, minute_data);
// Fill with garbage for more than ALG_MINUTE_DATA_FILE_LEN to force us to
// chop off old data.
s_capture_dls_records = false;
uint32_t max_minutes = ALG_MINUTE_DATA_FILE_LEN * 3 / 2 / sizeof(AlgMinuteFileSample);
// Make sure it's a multiple of ALG_MINUTES_PER_RECORD
max_minutes = (max_minutes / ALG_MINUTES_PER_FILE_RECORD) * ALG_MINUTES_PER_FILE_RECORD;
for (int i = 0; i < max_minutes; i++) {
fake_rtc_increment_time(SECONDS_PER_MINUTE);
s_alg_next_steps = 0x1234;
AccelRawData samples[100] = { };
uint64_t timestamp = 0;
activity_algorithm_handle_accel(samples, s_sample_rate, timestamp);
s_alg_next_vmc = 0x11;
s_alg_next_orientation = 0x22;
AlgMinuteRecord minute_record = {};
activity_algorithm_minute_handler(rtc_get_time(), &minute_record);
}
// Get the size of the sleep data and make sure it is within the expected range
uint32_t num_records;
uint32_t data_bytes;
uint32_t minutes;
bool success = activity_algorithm_minute_file_info(false /*compact_first*/, &num_records,
&data_bytes, &minutes);
cl_assert(success);
cl_assert(data_bytes < ALG_MINUTE_DATA_FILE_LEN && data_bytes > ALG_MINUTE_DATA_FILE_LEN / 2);
cl_assert(minutes < ALG_MINUTE_FILE_MAX_ENTRIES * ALG_MINUTES_PER_FILE_RECORD);
cl_assert(minutes > ALG_MINUTE_FILE_MAX_ENTRIES * ALG_MINUTES_PER_FILE_RECORD / 2);
// Now, put in our expected data
// Call the minute handler, which computes the minute stats and saves them to data logging
// as well as the sleep PFS file.
time_t start_of_data_utc = rtc_get_time();
prv_feed_minute_data(num_minutes, minute_data, false /*simulate_bg_delays*/);
// Retrieve the minute data now
HealthMinuteData retrieve[num_minutes];
num_records = num_minutes;
time_t start = start_of_data_utc; // starting just past the minute to test that &start gets updated
activity_algorithm_get_minute_history(retrieve, &num_records, &start);
cl_assert_equal_i(num_records, num_minutes);
cl_assert_equal_i(start, start_of_data_utc);
for (int i = 0; i < num_minutes; i++) {
prv_assert_minute_data(&retrieve[i], &minute_data[i]);
}
}
// ---------------------------------------------------------------------------------------
// Test that the call to retrieve minute history from flash works correctly
void test_activity_algorithm_kraepelin__get_flash_minute_history(void) {
const int num_minutes = 4 * MINUTES_PER_HOUR;
time_t start_utc = rtc_get_time();
// Let's start time not on a 15 minute boundary to aggravate the get_minute logic
start_utc += 7 * SECONDS_PER_MINUTE;
rtc_set_time(start_utc);
// The test data
AlgMinuteDLSSample minute_data[num_minutes];
prv_create_test_data(num_minutes, minute_data);
// Call the minute handler, which computes the minute stats and saves them to data logging
// as well as to the sleep PFS file.
prv_feed_minute_data(num_minutes, minute_data, false /*simulate_bg_delays*/);
// Retrieve all of the minute data at once
HealthMinuteData retrieve[num_minutes * 2];
uint32_t num_records = num_minutes;
time_t start = start_utc + 5; // starting just past the minute to test that &start gets updated
activity_algorithm_get_minute_history(retrieve, &num_records, &start);
cl_assert_equal_i(num_records, num_minutes);
cl_assert_equal_i(start, start_utc);
for (int i = 0; i < num_minutes; i++) {
prv_assert_minute_data(&retrieve[i], &minute_data[i]);
}
// Retrieve, trying to start from a lot farther back, it should return the UTC of the first
// record available. Also ask for more than what is available
num_records = num_minutes * 2;
start = start_utc - SECONDS_PER_DAY;
activity_algorithm_get_minute_history(retrieve, &num_records, &start);
cl_assert_equal_i(num_records, num_minutes);
cl_assert_equal_i(start, start_utc);
for (int i = 0; i < num_minutes; i++) {
prv_assert_minute_data(&retrieve[i], &minute_data[i]);
}
// Retrieve a little (10 minutes) at a time
int num_records_left = num_minutes;
int num_records_found = 0;
start = start_utc;
while (num_records_left) {
uint32_t chunk;
chunk = MIN(10, num_records_left);
time_t first_ts = start;
activity_algorithm_get_minute_history(&retrieve[num_records_found], &chunk, &first_ts);
cl_assert_equal_i(start, first_ts);
num_records_left -= chunk;
num_records_found += chunk;
start += chunk * SECONDS_PER_MINUTE;
}
cl_assert_equal_i(num_records_found, num_minutes);
for (int i = 0; i < num_minutes; i++) {
prv_assert_minute_data(&retrieve[i], &minute_data[i]);
}
}
// ---------------------------------------------------------------------------------------
// Test that retrieving the most recent minute history works correctly. This test insures that
// we correctly include the minute history that has not yet been saved to flash
void test_activity_algorithm_kraepelin__get_ram_minute_history(void) {
const int num_minutes = 1 * MINUTES_PER_HOUR;
time_t start_utc = rtc_get_time();
// Let's start time not on a 15 minute boundary to aggravate the get_minute logic
start_utc += 7 * SECONDS_PER_MINUTE;
rtc_set_time(start_utc);
// The test data
AlgMinuteDLSSample minute_data[num_minutes];
prv_create_test_data(num_minutes, minute_data);
// Call the minute handler to feed in enough data to write to flash. This computes the minute
// stats and saves them to data logging as well as to the sleep PFS file.
prv_feed_minute_data(ALG_MINUTES_PER_FILE_RECORD, minute_data, false /*simulate_bg_delays*/);
uint32_t next_write_minute_idx = ALG_MINUTES_PER_FILE_RECORD;
// Once a minute, retrieve the last ALG_MINUTES_PER_RECORD minutes of data. We should
// get ALG_MINUTES_PER_RECORD records each time. We know that the activity algorithm code only
// writes a new minute data record to flash once every ALG_MINUTES_PER_RECORD minutes, but
// the records that are not yet saved to flash should be correctly retrieved from RAM.
time_t oldest_to_fetch = rtc_get_time() - (ALG_MINUTES_PER_FILE_RECORD * SECONDS_PER_MINUTE);
uint32_t next_read_minute_idx = 0;
for (int i = 0; i < ALG_MINUTES_PER_FILE_RECORD; i++, oldest_to_fetch += SECONDS_PER_MINUTE,
next_read_minute_idx++, next_write_minute_idx++) {
// Ask for the last ALG_MINUTES_PER_RECORD minutes of data
uint32_t num_records = ALG_MINUTES_PER_FILE_RECORD;
time_t start = oldest_to_fetch;
HealthMinuteData received_records[ALG_MINUTES_PER_FILE_RECORD];
activity_algorithm_get_minute_history(received_records, &num_records, &start);
cl_assert_equal_i(num_records, ALG_MINUTES_PER_FILE_RECORD);
cl_assert_equal_i(start, oldest_to_fetch);
printf("\nReceived %d minute records", (int)num_records);
for (int j = 0; j < num_records; j++) {
printf("\nRecord:%d, steps: %d", j, (int)received_records[j].steps);
}
// Verify the contents of the records
for (int j = 0; j < num_records; j++) {
prv_assert_minute_data(&received_records[j], &minute_data[next_read_minute_idx + j]);
}
// Advance another minute. It doesn't matter what data we feed in
prv_feed_minute_data(1, minute_data + next_write_minute_idx, false /*simulate_bg_delays*/);
}
// Let's add data for a partial minute and make sure that gets returned
const int exp_steps = 23;
oldest_to_fetch = rtc_get_time() - SECONDS_PER_MINUTE;
fake_rtc_increment_time(30); // 30 seconds
s_alg_next_steps = exp_steps;
AccelRawData samples[100] = { };
uint64_t timestamp = 0;
// Calling activity_algorithm_handle_accel() on our stub algorithm registers the new steps
// counts for this minute
activity_algorithm_handle_accel(samples, s_sample_rate, timestamp);
// Fetch the last whole minute plus this partial minute
time_t start = oldest_to_fetch;
uint32_t num_records = 2;
HealthMinuteData received_records[num_records];
activity_algorithm_get_minute_history(received_records, &num_records, &start);
cl_assert_equal_i(num_records, 2);
cl_assert_equal_i(start, oldest_to_fetch);
prv_assert_minute_data(&received_records[0],
&minute_data[next_read_minute_idx + ALG_MINUTES_PER_FILE_RECORD - 1]);
cl_assert_equal_i(received_records[1].steps, exp_steps);
}
// ---------------------------------------------------------------------------------------
// Test the logic that detects naps. This logic is performed by the
// prv_sleep_sessions_post_process() method.
void test_activity_algorithm_kraepelin__sleep_post_process(void) {
// NOTE: All tests by default start at 5pm. Let's advance time to 9pm to give us more
// time to test the various nap scenarios
time_t now_utc = rtc_get_time();
now_utc += 4 * SECONDS_PER_HOUR;
rtc_set_time(now_utc);
time_t start_of_today = time_util_get_midnight_of(now_utc);
{ // Create a 2 hour session at 1pm ==> should be a nap
ActivitySession sessions[] = {
{
.start_utc = start_of_today + (13 * SECONDS_PER_HOUR), // 1pm
.length_min = 2 * MINUTES_PER_HOUR,
.type = ActivitySessionType_Sleep,
},
{
.start_utc = start_of_today + (13 * SECONDS_PER_HOUR) + (15 * SECONDS_PER_MINUTE), // 1:15pm
.length_min = 20,
.type = ActivitySessionType_RestfulSleep,
},
};
uint16_t session_entries = ARRAY_LENGTH(sessions);
activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
cl_assert_equal_i(sessions[0].type, ActivitySessionType_Nap);
cl_assert_equal_i(sessions[1].type, ActivitySessionType_RestfulNap);
}
{ // Create a 4 hour session at 1pm ==> should be regular sleep
ActivitySession sessions[] = {
{
.start_utc = start_of_today + (13 * (SECONDS_PER_HOUR)), // 1pm
.length_min = 4 * MINUTES_PER_HOUR,
.type = ActivitySessionType_Sleep,
},
};
uint16_t session_entries = ARRAY_LENGTH(sessions);
activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
cl_assert_equal_i(sessions[0].type, ActivitySessionType_Sleep);
}
{ // Create two 2 hour sessions, they should both be considered as separate naps
ActivitySession sessions[] = {
{
.start_utc = start_of_today + (13 * SECONDS_PER_HOUR), // 1pm
.length_min = 2 * MINUTES_PER_HOUR,
.type = ActivitySessionType_Sleep,
},
{
.start_utc = start_of_today + (17 * SECONDS_PER_HOUR), // 5pm
.length_min = 2 * MINUTES_PER_HOUR,
.type = ActivitySessionType_Sleep,
},
};
uint16_t session_entries = ARRAY_LENGTH(sessions);
activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
cl_assert_equal_i(sessions[0].type, ActivitySessionType_Nap);
cl_assert_equal_i(sessions[1].type, ActivitySessionType_Nap);
}
{ // Create a 2 hour session that ends after 9pm ==> should be regular sleep
ActivitySession sessions[] = {
{
.start_utc = start_of_today + (20 * SECONDS_PER_HOUR), // 8pm
.length_min = 2 * MINUTES_PER_HOUR,
.type = ActivitySessionType_Sleep,
},
};
uint16_t session_entries = ARRAY_LENGTH(sessions);
activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
cl_assert_equal_i(sessions[0].type, ActivitySessionType_Sleep);
}
{ // Create a 2 hour session that starts before 12pm ==> should be regular sleep
ActivitySession sessions[] = {
{
.start_utc = start_of_today + (11 * SECONDS_PER_HOUR), // 11am
.length_min = 2 * MINUTES_PER_HOUR,
.type = ActivitySessionType_Sleep,
},
};
uint16_t session_entries = ARRAY_LENGTH(sessions);
activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
cl_assert_equal_i(sessions[0].type, ActivitySessionType_Sleep);
}
{ // Create a 2 hour session that is still on-going - should register as normal sleep
time_t sleep_start_utc = now_utc - (2 * SECONDS_PER_HOUR);
ActivitySession sessions[] = {
{
.start_utc = sleep_start_utc,
.length_min = 2 * MINUTES_PER_HOUR,
.type = ActivitySessionType_Sleep,
.ongoing = true,
},
{
.start_utc = sleep_start_utc + (15 * SECONDS_PER_MINUTE),
.length_min = 20,
.type = ActivitySessionType_RestfulSleep,
.ongoing = true,
},
};
uint16_t session_entries = ARRAY_LENGTH(sessions);
activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
cl_assert_equal_i(sessions[0].type, ActivitySessionType_Sleep);
cl_assert_equal_i(sessions[1].type, ActivitySessionType_RestfulSleep);
}
{ // Create a 2h 39m session that starts at 11:59pm ==> should be regular sleep
ActivitySession sessions[] = {
{
.start_utc = start_of_today - (1 * SECONDS_PER_MINUTE), // 11:59pm
.length_min = (2 * MINUTES_PER_HOUR) + 39,
.type = ActivitySessionType_Sleep,
},
};
uint16_t session_entries = ARRAY_LENGTH(sessions);
activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
cl_assert_equal_i(sessions[0].type, ActivitySessionType_Sleep);
}
}
// ---------------------------------------------------------------------------------------
// Test to make sure we don't get steps counted while sleeping
void test_activity_algorithm_kraepelin__steps_during_sleep(void) {
const int num_minutes = 120;
// The test data
AlgMinuteDLSSample minute_data[num_minutes];
prv_create_test_data(num_minutes, minute_data);
// Zero out the first hour of data. The sleep algorithm takes an hour to figure out that
// you are sleeping, so it has no chance of zeroing out all the steps in that first hour.
for (int i = 0; i < 60; i++) {
minute_data[i].base.steps = 0;
}
// -------------------------------------------------------------------------------
// Set to not sleeping
s_kalg_sleep_start_utc = 0;
s_kalg_sleep_m = 0;
activity_algorithm_metrics_changed_notification();
uint16_t steps_awake_60m;
uint16_t steps_awake_100m;
uint16_t steps_awake_120m;
// Call the minute handler, which should zero out steps that occur while sleeping
prv_feed_minute_data(60, &minute_data[0], false /*simulate_bg_delays*/);
activity_algorithm_get_steps(&steps_awake_60m);
prv_feed_minute_data(40, &minute_data[60], false /*simulate_bg_delays*/);
activity_algorithm_get_steps(&steps_awake_100m);
prv_feed_minute_data(20, &minute_data[100], false /*simulate_bg_delays*/);
activity_algorithm_get_steps(&steps_awake_120m);
// We should get steps counted while not sleeping
printf("\nWhile awake: ");
printf("\n Counted %d steps first 60m", steps_awake_60m);
printf("\n Counted %d steps next 40m", steps_awake_100m - steps_awake_60m);
printf("\n Counted %d steps last 20m", steps_awake_120m - steps_awake_100m);
printf("\n Total: %d\n", steps_awake_120m);
// Compute the expected number of steps
int exp_steps = 0;
for (int i = 0; i < num_minutes; i++) {
exp_steps += minute_data[i].base.steps;
}
cl_assert_equal_i(steps_awake_120m, exp_steps);
// -------------------------------------------------------------------------------
// Try again while sleeping
time_t start_utc = rtc_get_time();
// Set to sleeping for the first 100 minutes
s_kalg_sleep_start_utc = start_utc;
s_kalg_sleep_m = 100;
activity_algorithm_metrics_changed_notification();
uint16_t steps_asleep_60m;
uint16_t steps_asleep_100m;
uint16_t steps_asleep_120m;
// Call the minute handler, which should zero out steps that occur while sleeping
prv_feed_minute_data(60, &minute_data[0], false /*simulate_bg_delays*/);
activity_algorithm_get_steps(&steps_asleep_60m);
prv_feed_minute_data(40, &minute_data[60], false /*simulate_bg_delays*/);
activity_algorithm_get_steps(&steps_asleep_100m);
prv_feed_minute_data(20, &minute_data[100], false /*simulate_bg_delays*/);
activity_algorithm_get_steps(&steps_asleep_120m);
// We should get steps counted while not sleeping
printf("\nWhile asleep in the first 100m: ");
printf("\n Counted %d steps first 60m", steps_asleep_60m);
printf("\n Counted %d steps next 40m", steps_asleep_100m - steps_asleep_60m);
printf("\n Counted %d steps last 20m", steps_asleep_120m - steps_asleep_100m);
printf("\n Total: %d\n", steps_asleep_120m);
// We should only get the steps counted from the last 20 minutes after waking
cl_assert_equal_i(steps_asleep_120m, steps_awake_120m - steps_awake_100m);
}
// ---------------------------------------------------------------------------------------
// Test to make sure that the minute data we save has no steps during sleep
void test_activity_algorithm_kraepelin__minute_data_steps_during_sleep(void) {
const int num_minutes = 120;
// The test data
AlgMinuteDLSSample minute_data[num_minutes];
prv_create_test_data(num_minutes, minute_data);
// Zero out the first hour of data. The sleep algorithm takes an hour to figure out that
// you are sleeping, so it has no chance of zeroing out all the steps in that first hour.
for (int i = 0; i < 60; i++) {
minute_data[i].base.steps = 0;
}
time_t start_utc = rtc_get_time();
// Set to sleeping for the first 100 minutes
s_kalg_sleep_start_utc = start_utc;
s_kalg_sleep_m = 100;
// Write the data out
prv_feed_minute_data(num_minutes, minute_data, false /*simulate_bg_delays*/);
// Retrieve all the minute data and verify the contents
HealthMinuteData retrieve[num_minutes];
uint32_t num_records = num_minutes;
time_t start = start_utc;
activity_algorithm_get_minute_history(retrieve, &num_records, &start);
cl_assert_equal_i(num_records, num_minutes);
cl_assert_equal_i(start, start_utc);
for (int i = 0; i < num_minutes; i++) {
// If this is during the sleep period, steps should be 0
if (i < 100) {
cl_assert_equal_i(retrieve[i].steps, 0);
} else {
cl_assert_equal_i(retrieve[i].steps, minute_data[i].base.steps);
}
cl_assert_equal_i(retrieve[i].orientation, minute_data[i].base.orientation);
cl_assert_equal_i(retrieve[i].vmc, minute_data[i].base.vmc);
cl_assert_equal_i(retrieve[i].light, ambient_light_level_to_enum(
minute_data[i].base.light * ALG_RAW_LIGHT_SENSOR_DIVIDE_BY));
cl_assert_equal_i(retrieve[i].heart_rate_bpm, minute_data[i].heart_rate_bpm);
}
}
@@ -0,0 +1,235 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "services/normal/activity/activity.h"
#include "services/normal/activity/activity_private.h"
#include "services/normal/activity/activity_calculators.h"
#include "util/units.h"
#include <stdint.h>
// Fakes
static uint8_t s_age_years;
uint8_t activity_prefs_get_age_years(void) {
return s_age_years;
}
static ActivityGender s_gender;
ActivityGender activity_prefs_get_gender(void) {
return s_gender;
}
static uint16_t s_weight_dag;
uint16_t activity_prefs_get_weight_dag(void) {
return s_weight_dag;
}
static uint16_t s_height_mm;
uint16_t activity_prefs_get_height_mm(void) {
return s_height_mm;
}
typedef enum {
Human_TallMale,
Human_ShortMale,
Human_TallFemale,
Human_ShortFemale,
Human_Count,
} Human;
typedef struct HumanPrefs {
uint8_t age_years;
ActivityGender gender;
uint16_t weight_dag;
uint16_t height_mm;
} HumanPrefs;
#define ACTIVITY_DEFAULT_HEIGHT_MM 1620 // 5'3.8"
// dag - decagram (10 g)
#define ACTIVITY_DEFAULT_WEIGHT_DAG 7539 // 166.2 lbs
#define ACTIVITY_DEFAULT_GENDER ActivityGenderFemale
#define ACTIVITY_DEFAULT_AGE_YEARS 30
static void prv_set_user(Human type) {
const HumanPrefs types[Human_Count] = {
[Human_TallMale] = {30, ActivityGenderMale, 7539, 1900},
[Human_ShortMale] = {30, ActivityGenderMale, 4536, 1620},
[Human_TallFemale] = {30, ActivityGenderFemale, 7539, 1900},
[Human_ShortFemale] = {30, ActivityGenderFemale, 4536, 1620},
};
s_age_years = types[type].age_years;
s_gender = types[type].gender;
s_weight_dag = types[type].weight_dag;
s_height_mm = types[type].height_mm;
}
#define MM_PER_METER 1000
#define M_PER_KM 1000
// =============================================================================================
// Start of unit tests
void test_activity_calculators__initialize(void) {
}
// ---------------------------------------------------------------------------------------
void test_activity_calculators__cleanup(void) {
}
// ---------------------------------------------------------------------------------------
void test_activity_calculators__distance(void) {
uint32_t steps;
uint32_t time_ms;
prv_set_user(Human_TallMale);
const int walking_cadence_spm = 100;
// Do an normal walk for 12 mins. An average person should cover ~1km
const int walk_time = 12;
steps = walking_cadence_spm * walk_time;
time_ms = walk_time * SECONDS_PER_MINUTE * MS_PER_SECOND;
int easy_walk_distance_m = activity_private_compute_distance_mm(steps, time_ms) / MM_PER_METER;
cl_assert_within(easy_walk_distance_m, 900, 1100);
// Walk for 12 mins again, but this time faster. More distance should be covered
steps = (walking_cadence_spm * 1.2) * walk_time;
time_ms = walk_time * SECONDS_PER_MINUTE * MS_PER_SECOND;
int fast_walk_distance_m = activity_private_compute_distance_mm(steps, time_ms) / MM_PER_METER;
cl_assert_gt(fast_walk_distance_m, easy_walk_distance_m);
// Walk for a long time. People can walk at roughly 5km/h, so we should be close to 50km
const int long_walk_time = 10 * MINUTES_PER_HOUR;
steps = walking_cadence_spm * long_walk_time;
time_ms = long_walk_time * SECONDS_PER_MINUTE * MS_PER_SECOND;
int long_walk_distance_m = activity_private_compute_distance_mm(steps, time_ms) / MM_PER_METER;
cl_assert_within(long_walk_distance_m, 48000, 52000);
// A typical cadence is roughly 165 steps per minute
const int running_cadence_spm = 165;
// Running for 25 minutes should come out to roughly 5km
const int run_time = 25;
steps = running_cadence_spm * run_time;
time_ms = run_time * SECONDS_PER_MINUTE * MS_PER_SECOND;
int normal_run_distance_m = activity_private_compute_distance_mm(steps, time_ms) / MM_PER_METER;
cl_assert_within(normal_run_distance_m, 4500, 5500);
// Running for 25 minutes again, but this time faster
steps = (running_cadence_spm * 1.15) * run_time;
time_ms = run_time * SECONDS_PER_MINUTE * MS_PER_SECOND;
int fast_run_distance_m = activity_private_compute_distance_mm(steps, time_ms) / MM_PER_METER;
cl_assert_within(fast_run_distance_m, 6500, 7000);
cl_assert_gt(fast_run_distance_m, normal_run_distance_m);
// Run for 3.5 hours. This is a reasonable marathon time
const int long_run_time = 3 * MINUTES_PER_HOUR + 30;
steps = running_cadence_spm * long_run_time;
time_ms = long_run_time * SECONDS_PER_MINUTE * MS_PER_SECOND;
int long_run_distance_m = activity_private_compute_distance_mm(steps, time_ms) / MM_PER_METER;
cl_assert_within(long_run_distance_m, 40000, 44000);
// Now make a shorter guy run for the same time.
// He should be in the same ballpark but cover less distance
prv_set_user(Human_ShortMale);
int short_guy_distance = activity_private_compute_distance_mm(steps, time_ms) / MM_PER_METER;
cl_assert_within(short_guy_distance, 36000, 44000);
cl_assert_gt(long_run_distance_m, short_guy_distance);
// And finally throw in a specific value so that anyone who touches the function will have to
// check up on the unit tests
cl_assert_equal_i(short_guy_distance, 36845);
}
// ---------------------------------------------------------------------------------------
void test_activity_calculators__active_calories(void) {
uint32_t distance_mm;
uint32_t time_ms;
prv_set_user(Human_ShortMale);
// Walk 1km in 12 minutes
distance_mm = 1 * M_PER_KM * MM_PER_METER;
time_ms = 12 * SECONDS_PER_MINUTE * MS_PER_SECOND;
int walk_calories = activity_private_compute_active_calories(distance_mm, time_ms) / ACTIVITY_CALORIES_PER_KCAL;
cl_assert_within(walk_calories, 20, 25); // This seems a little low, but not un-reasonable
// Run 1km in 5 minutes. This should burn more calories than walking
distance_mm = 1 * M_PER_KM * MM_PER_METER;
time_ms = 5 * SECONDS_PER_MINUTE * MS_PER_SECOND;
int run_calories = activity_private_compute_active_calories(distance_mm, time_ms) / ACTIVITY_CALORIES_PER_KCAL;
cl_assert_within(run_calories, 40, 60); // This also seems a little low, but not un-reasonable
cl_assert_gt(run_calories, walk_calories);
// Run 5km in 25 minutes
distance_mm = 5 * M_PER_KM * MM_PER_METER;
time_ms = 25 * SECONDS_PER_MINUTE * MS_PER_SECOND;
int five_k_calories = activity_private_compute_active_calories(distance_mm, time_ms) / ACTIVITY_CALORIES_PER_KCAL;
cl_assert_within(five_k_calories, 220, 250);
cl_assert_gt(five_k_calories, run_calories);
// PG: I went for the following run last night and my garmin watch said I burned 550 calories
prv_set_user(Human_TallMale);
distance_mm = 7 * M_PER_KM * MM_PER_METER;
time_ms = 30 * SECONDS_PER_MINUTE * MS_PER_SECOND;
int quick_run_calories = activity_private_compute_active_calories(distance_mm, time_ms) / ACTIVITY_CALORIES_PER_KCAL;
cl_assert_within(quick_run_calories, 520, 580);
// Run a marathon
distance_mm = 42 * M_PER_KM * MM_PER_METER;
time_ms = 3 * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
int long_run_calories = activity_private_compute_active_calories(distance_mm, time_ms) / ACTIVITY_CALORIES_PER_KCAL;
cl_assert_within(long_run_calories, 3000, 3200);
// And finally throw in a specific value so that anyone who touches the function will have to
// check up on the unit tests
cl_assert_equal_i(long_run_calories, 3172);
}
// ---------------------------------------------------------------------------------------
void test_activity_calculators__inactive_calories(void) {
// People burn roughly 2000 (women) - 2400 (men) a day. This number includes active calories
// so I would expect the values we get to be less than that. I don't know enough to make
// better real world analogies though...
uint32_t long_time_m = 24 * MINUTES_PER_HOUR;
uint32_t short_time_m = 5;
prv_set_user(Human_ShortMale);
cl_assert_equal_i(1321, activity_private_compute_resting_calories(long_time_m) / ACTIVITY_CALORIES_PER_KCAL);
cl_assert_equal_i(4, activity_private_compute_resting_calories(short_time_m) / ACTIVITY_CALORIES_PER_KCAL);
prv_set_user(Human_TallMale);
cl_assert_equal_i(1796, activity_private_compute_resting_calories(long_time_m)/ ACTIVITY_CALORIES_PER_KCAL);
cl_assert_equal_i(6, activity_private_compute_resting_calories(short_time_m) / ACTIVITY_CALORIES_PER_KCAL);
prv_set_user(Human_ShortFemale);
cl_assert_equal_i(1155, activity_private_compute_resting_calories(long_time_m) / ACTIVITY_CALORIES_PER_KCAL);
cl_assert_equal_i(4, activity_private_compute_resting_calories(short_time_m) / ACTIVITY_CALORIES_PER_KCAL);
prv_set_user(Human_TallFemale);
cl_assert_equal_i(1630, activity_private_compute_resting_calories(long_time_m) / ACTIVITY_CALORIES_PER_KCAL);
cl_assert_equal_i(5, activity_private_compute_resting_calories(short_time_m) / ACTIVITY_CALORIES_PER_KCAL);
}
@@ -0,0 +1,912 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "services/normal/activity/activity.h"
#include "services/normal/activity/activity_insights.h"
#include "services/normal/activity/activity_private.h"
#include "services/normal/activity/insights_settings.h"
#include "services/normal/filesystem/pfs.h"
#include "util/attributes.h"
#include <stdint.h>
#include "clar.h"
// Stubs
#include "stubs_analytics.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_state.h"
#include "stubs_attribute.h"
#include "stubs_event_service_client.h"
#include "stubs_health_db.h"
#include "stubs_health_util.h"
#include "stubs_i18n.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pebble_tasks.h"
#include "stubs_rand_ptr.h"
#include "stubs_stringlist.h"
#include "stubs_system_task.h"
// Fakes
#include "fake_kernel_services_notifications.h"
#include "fake_pbl_malloc.h"
#include "fake_rtc.h"
#include "fake_settings_file.h"
// We start time out at 5pm on Jan 1, 2015 for all of these tests
static struct tm s_init_time_tm = {
// Thursday, Jan 1, 2015, 10:00am
.tm_hour = 10,
.tm_mday = 1,
.tm_mon = 0,
.tm_year = 115
};
#define ACTIVE_MINUTES 2
#define AVERAGE_STEPS 1000
#define HIGH_STEPS 2000
#define MAX_ACTIVITY_SESSIONS 24
extern void prv_calculate_metric_history_stats(ActivityMetric metric,
ActivityInsightMetricHistoryStats *stats);
// =========================================================================================
// Activity stubs / fakes
typedef struct StaticData {
ActivityScalarStore steps_per_minute;
int32_t metric_history[ActivityMetricNumMetrics][ACTIVITY_HISTORY_DAYS];
// Enough room for 3 days worth of sessions
ActivitySession activity_sessions[3 * MAX_ACTIVITY_SESSIONS];
uint32_t num_sessions;
int pins_added;
int pins_removed;
int notifs_shown;
} StaticData;
static StaticData s_data = {};
bool activity_prefs_activity_insights_are_enabled(void) {
return true;
}
bool activity_prefs_sleep_insights_are_enabled(void) {
return true;
}
bool activity_get_metric(ActivityMetric metric, uint32_t history_len, int32_t *history) {
memcpy(history, &s_data.metric_history[metric], history_len * sizeof(int32_t));
return true;
}
// Update the sleep metrics based on the current set of sleep sessions for today
static void prv_update_sleep_metrics(void) {
ActivitySession activity_sessions[MAX_ACTIVITY_SESSIONS];
uint32_t num_sessions = MAX_ACTIVITY_SESSIONS;
activity_get_sessions(&num_sessions, activity_sessions);
uint32_t total_seconds = 0;
time_t sleep_enter_utc = -1;
time_t sleep_exit_utc = -1;
for (uint32_t i = 0; i < num_sessions; i++) {
time_t exit_utc = activity_sessions[i].start_utc
+ activity_sessions[i].length_min * SECONDS_PER_MINUTE;
if (activity_sessions[i].type == ActivitySessionType_Sleep) {
if (sleep_enter_utc == -1) {
sleep_enter_utc = activity_sessions[i].start_utc;
}
if (sleep_exit_utc == -1 || exit_utc > sleep_exit_utc) {
sleep_exit_utc = exit_utc;
}
}
total_seconds += activity_sessions[i].length_min * SECONDS_PER_MINUTE;
}
s_data.metric_history[ActivityMetricSleepEnterAtSeconds][0] =
time_util_get_minute_of_day(sleep_enter_utc) * SECONDS_PER_MINUTE;
s_data.metric_history[ActivityMetricSleepExitAtSeconds][0] =
time_util_get_minute_of_day(sleep_exit_utc) * SECONDS_PER_MINUTE;
s_data.metric_history[ActivityMetricSleepTotalSeconds][0] = total_seconds;
}
void activity_sessions_prv_get_sleep_bounds_utc(time_t now_utc, time_t *enter_utc, time_t *exit_utc) {
ActivitySession activity_sessions[MAX_ACTIVITY_SESSIONS];
uint32_t num_sessions = MAX_ACTIVITY_SESSIONS;
activity_get_sessions(&num_sessions, activity_sessions);
uint32_t total_seconds = 0;
*enter_utc = 0;
*exit_utc = 0;
for (uint32_t i = 0; i < num_sessions; i++) {
if (activity_sessions[i].type != ActivitySessionType_Sleep) {
continue;
}
if (*enter_utc == 0) {
*enter_utc = activity_sessions[i].start_utc;
}
time_t session_exit_utc = activity_sessions[i].start_utc
+ activity_sessions[i].length_min * SECONDS_PER_MINUTE;
if (*exit_utc == 0 || session_exit_utc > *exit_utc) {
*exit_utc = session_exit_utc;
}
}
}
// Appends a new sleep session to the sleep sessions array and increments the current SleepExit and
// SleepTotal metrics accordingly
void prv_add_sleep_or_nap_session(ActivitySessionType session_type, double offset_hours,
double length_hours) {
int32_t offset_sec = offset_hours * SECONDS_PER_HOUR;
uint32_t length_min = length_hours * MINUTES_PER_HOUR;
uint32_t length_sec = length_hours * SECONDS_PER_HOUR;
int prev = s_data.num_sessions - 1;
time_t midnight = time_util_get_midnight_of(rtc_get_time());
time_t previous_exit_utc;
if (s_data.num_sessions > 0) {
previous_exit_utc = s_data.activity_sessions[prev].start_utc
+ (s_data.activity_sessions[prev].length_min * SECONDS_PER_MINUTE);
} else {
previous_exit_utc = midnight;
}
time_t start_utc = previous_exit_utc + offset_sec;
if (start_utc > rtc_get_time()) {
printf("now_utc: %ld, start_utc: %ld\n", rtc_get_time(), start_utc);
}
cl_assert(start_utc <= rtc_get_time());
if (start_utc + length_sec > rtc_get_time()) {
printf("now_utc: %ld, end_utc: %ld\n", rtc_get_time(), start_utc + length_sec);
}
cl_assert(start_utc + length_sec <= rtc_get_time());
s_data.activity_sessions[s_data.num_sessions++] = (ActivitySession) {
.type = session_type,
.length_min = length_min,
.start_utc = start_utc,
};
// Update 'current' metrics
prv_update_sleep_metrics();
}
// Appends a new sleep session to the sleep sessions array and increments the current SleepExit and
// SleepTotal metrics accordingly
void prv_add_sleep_session(double offset_hours, double length_hours) {
prv_add_sleep_or_nap_session(ActivitySessionType_Sleep, offset_hours, length_hours);
}
// Appends a new nap session to the sleep sessions array and increments the current SleepExit and
// SleepTotal metrics accordingly
void prv_add_nap_session(double offset_hours, double length_hours) {
prv_add_sleep_or_nap_session(ActivitySessionType_Nap, offset_hours, length_hours);
}
void prv_add_walk_session(double offset_hours, double length_hours) {
const uint32_t length_min = length_hours * MINUTES_PER_HOUR;
s_data.activity_sessions[s_data.num_sessions++] = (ActivitySession) {
.type = ActivitySessionType_Walk,
.length_min = length_min,
.start_utc = rtc_get_time(),
.step_data = {
.steps = length_min * 60,
.active_kcalories = length_min * 2,
.resting_kcalories = length_min / 10,
.distance_meters = (length_min * 1000) / 30,
},
};
}
bool activity_get_sessions(uint32_t *session_entries, ActivitySession *sessions) {
// Only return the sleep sessions that belong to "today"
time_t start_of_today_utc = time_util_get_midnight_of(rtc_get_time());
int last_sleep_second_of_day = ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY * SECONDS_PER_MINUTE;
time_t sleep_earliest_end_utc = start_of_today_utc
- (SECONDS_PER_DAY - last_sleep_second_of_day);
uint32_t num_sessions_returned = 0;
for (uint32_t i = 0; i < s_data.num_sessions; i++) {
if (num_sessions_returned >= *session_entries) {
// No more room
break;
}
time_t session_end = s_data.activity_sessions[i].start_utc
+ s_data.activity_sessions[i].length_min * SECONDS_PER_MINUTE;
if (session_end >= sleep_earliest_end_utc) {
// This session should be included in today's sessions
sessions[num_sessions_returned++] = s_data.activity_sessions[i];
}
}
*session_entries = num_sessions_returned;
return true;
}
SettingsFile *activity_private_settings_open(void) {
static SettingsFile file = {};
return &file;
}
void activity_private_settings_close(SettingsFile *file) {
return;
}
bool activity_get_step_averages(DayInWeek day_of_week, ActivityMetricAverages *averages) {
return false;
}
static time_t s_activation_time = 0;
time_t activity_prefs_get_activation_time(void) {
return s_activation_time;
}
static void prv_set_activation_time(time_t activation_time) {
s_activation_time = activation_time;
}
static uint32_t s_activity_activation_delay_insight_bitmask = 0;
bool activity_prefs_has_activation_delay_insight_fired(ActivationDelayInsightType type) {
return (s_activity_activation_delay_insight_bitmask & (1 << type));
}
void activity_prefs_set_activation_delay_insight_fired(ActivationDelayInsightType type) {
s_activity_activation_delay_insight_bitmask |= (1 << type);
}
static int s_health_app_opened_version = 0;
uint8_t activity_prefs_get_health_app_opened_version(void) {
return s_health_app_opened_version;
}
ActivityScalarStore activity_metrics_prv_steps_per_minute(void) {
return s_data.steps_per_minute;
}
// =========================================================================================
// PFS stubs
static PFSFileChangedCallback pfs_watch_cb = NULL;
PFSCallbackHandle pfs_watch_file(const char* filename, PFSFileChangedCallback callback,
uint8_t event_flags, void* data) {
pfs_watch_cb = callback;
return NULL;
}
void pfs_unwatch_file(PFSCallbackHandle cb_handle) {
pfs_watch_cb = NULL;
}
// =========================================================================================
// Timeline item stubs
static TimelineItem s_item = {};
TimelineItem *timeline_item_create_with_attributes(time_t timestamp, uint16_t duration,
TimelineItemType type, LayoutId layout,
AttributeList *attr_list,
TimelineItemActionGroup *action_group) {
uuid_generate(&s_item.header.id);
return &s_item;
}
void timeline_item_destroy(TimelineItem* item) {
return;
}
// =========================================================================================
// Timeline stubs
static Uuid s_last_timeline_id;
bool timeline_add(TimelineItem *item) {
s_last_timeline_id = item->header.id;
s_data.pins_added++;
return true;
}
bool timeline_remove(Uuid *id) {
s_data.pins_removed++;
return true;
}
bool timeline_exists(Uuid *id) {
return true;
}
// =========================================================================================
// Notification stubs
void notification_storage_store(TimelineItem* notification) {
s_data.notifs_shown++;
}
// Helpers
static void prv_set_time(const struct tm *input) {
struct tm time_tm = *input;
time_t utc_sec = mktime(&time_tm);
rtc_set_time(utc_sec);
s_activity_activation_delay_insight_bitmask = 0;
s_activation_time = 0;
s_activity_activation_delay_insight_bitmask = 0;
}
// =============================================================================================
// Start of unit tests
void test_activity_insights__initialize(void) {
prv_set_time(&s_init_time_tm);
fake_kernel_services_notifications_reset();
s_activation_time = 0;
s_health_app_opened_version = 0;
s_data = (StaticData) {};
}
// ---------------------------------------------------------------------------------------
void test_activity_insights__cleanup(void) {
fake_settings_file_reset();
}
// ---------------------------------------------------------------------------------------
// Test that we correctly calculate the statistics (# days of history, median, etc)
void test_activity_insights__calculate_metric_history_stats(void) {
// Construct history
static const int32_t complete_history[ACTIVITY_HISTORY_DAYS] = {
1234, // This value is ignored since it's loaded in as the current value
6233, 4277, 9857, 4737, 6540, 719, 9917, 7019, 6347, 4704, 5050, 8370, 4200, 8284, 6664,
9177, 9734, 2330, 3951, 1568, 871, 776, 8751, 987, 7813, 772, 5079, 7438, 428
};
memcpy(&s_data.metric_history[ActivityMetricStepCount], complete_history,
sizeof(complete_history));
ActivityInsightMetricHistoryStats stats;
prv_calculate_metric_history_stats(ActivityMetricStepCount, &stats);
cl_assert_equal_i(stats.median, 5079);
cl_assert_equal_i(stats.total_days, 29);
cl_assert_equal_i(stats.consecutive_days, 29);
// Test sparse history
static const int32_t sparse_history[ACTIVITY_HISTORY_DAYS] = {
1234, // This value is ignored since it's loaded in as the current day
6233, 4277, 9857, 0, 6540, 719, 0, 0, 0, 0, 0, 0, 0, 0, 6664, 9177, 0, 2330, 3951, 1568,
871, 0, 8751, 0, 7813, 772, 0, 7438, 428
};
memcpy(&s_data.metric_history[ActivityMetricStepCount], sparse_history,
sizeof(sparse_history));
prv_calculate_metric_history_stats(ActivityMetricStepCount, &stats);
cl_assert_equal_i(stats.median, 4277);
cl_assert_equal_i(stats.total_days, 16);
cl_assert_equal_i(stats.consecutive_days, 3);
}
// ---------------------------------------------------------------------------------------
// Test that the sleep reward triggers when it should, and doesn't trigger when it shouldn't
void test_activity_insights__sleep_reward(void) {
// Use reasonable insight settings
const ActivityScalarStore AVERAGE_SLEEP = 5 * MINUTES_PER_HOUR;
const ActivityScalarStore GOOD_SLEEP = 8 * MINUTES_PER_HOUR;
static const int32_t sleep_history[ACTIVITY_HISTORY_DAYS] = {
GOOD_SLEEP, // This is 'today'
GOOD_SLEEP, // User has had good sleep for past 3 nights
GOOD_SLEEP,
GOOD_SLEEP,
AVERAGE_SLEEP, // Average sleep to make sure our median is fairly low
AVERAGE_SLEEP,
AVERAGE_SLEEP,
AVERAGE_SLEEP,
AVERAGE_SLEEP,
AVERAGE_SLEEP,
AVERAGE_SLEEP,
AVERAGE_SLEEP,
AVERAGE_SLEEP
};
memcpy(&s_data.metric_history[ActivityMetricSleepTotalSeconds], sleep_history,
sizeof(sleep_history));
activity_insights_init(rtc_get_time());
// Make sure we don't trigger while still asleep
s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateLightSleep;
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
// Make sure we don't trigger as soon as we're awake
s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateAwake;
s_data.metric_history[ActivityMetricSleepStateSeconds][0] = 1 * SECONDS_PER_HOUR;
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
// Make sure we do not trigger, the insights are disabled
s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateAwake;
s_data.metric_history[ActivityMetricSleepStateSeconds][0] = 2 * SECONDS_PER_HOUR;
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
// Advance the clock some and make sure we still don't get notifications
for (int i = 0; i < 100; ++i) {
rtc_set_time(rtc_get_time() + 2 * SECONDS_PER_MINUTE);
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
}
// // These tests only make sense if the insights are enabled
// // Now we shouldn't see another notification for the next 6 days
// for (int i = 0; i < 6; ++i) {
// rtc_set_time(rtc_get_time() + SECONDS_PER_DAY);
// activity_insights_recalculate_stats();
// activity_insights_process_sleep_data(rtc_get_time());
// cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
// }
//
// rtc_set_time(rtc_get_time() + SECONDS_PER_DAY);
// activity_insights_recalculate_stats();
// activity_insights_process_sleep_data(rtc_get_time());
// cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 2);
//
// // Make sure we don't trigger if we didn't get enough sleep
// rtc_set_time(rtc_get_time() + 7 * SECONDS_PER_DAY);
// activity_insights_recalculate_stats();
// s_data.metric_history[ActivityMetricSleepTotalSeconds][0] = AVERAGE_SLEEP;
// activity_insights_process_sleep_data(rtc_get_time());
// cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 2);
//
// // Fall back asleep, make sure we get the reward
// s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateLightSleep;
// activity_insights_process_sleep_data(rtc_get_time());
// s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateAwake;
// s_data.metric_history[ActivityMetricSleepTotalSeconds][0] = GOOD_SLEEP;
// activity_insights_process_sleep_data(rtc_get_time());
// cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 3);
//
// // Make sure setting enable to false actually disables things
// rtc_set_time(rtc_get_time() + 7 * SECONDS_PER_DAY);
// activity_insights_recalculate_stats();
// ActivityInsightSettings disabled_sleep;
// activity_insights_settings_read(ACTIVITY_INSIGHTS_SETTINGS_SLEEP_REWARD, &disabled_sleep);
// disabled_sleep.enabled = false,
// settings_file_set(NULL, /* fake settings file don't care */
// ACTIVITY_INSIGHTS_SETTINGS_SLEEP_REWARD, strlen(ACTIVITY_INSIGHTS_SETTINGS_SLEEP_REWARD),
// &disabled_sleep, sizeof(disabled_sleep));
// pfs_watch_cb(NULL); // Update the settings cache
// activity_insights_process_sleep_data(rtc_get_time());
// cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 3);
}
static void prv_minute_update(int iterations) {
for ( ; iterations > 0; --iterations) {
rtc_set_time(rtc_get_time() + SECONDS_PER_MINUTE);
activity_insights_process_minute_data(rtc_get_time());
}
}
// ---------------------------------------------------------------------------------------
void prv_set_step_history_avg() {
// History with low median
static const int32_t step_history[ACTIVITY_HISTORY_DAYS] = {
AVERAGE_STEPS, // This is 'today'
AVERAGE_STEPS,
AVERAGE_STEPS,
AVERAGE_STEPS,
AVERAGE_STEPS,
AVERAGE_STEPS,
AVERAGE_STEPS,
AVERAGE_STEPS,
AVERAGE_STEPS,
AVERAGE_STEPS,
AVERAGE_STEPS
};
memcpy(&s_data.metric_history[ActivityMetricStepCount], &step_history, sizeof(step_history));
}
void prv_set_sleep_history_avg() {
const ActivityScalarStore AVERAGE_SLEEP = 5 * MINUTES_PER_HOUR;
static const int32_t sleep_history[ACTIVITY_HISTORY_DAYS] = {
AVERAGE_SLEEP, // This is 'today'
AVERAGE_SLEEP, // Average sleep to make sure our median is fairly low
AVERAGE_SLEEP,
AVERAGE_SLEEP,
AVERAGE_SLEEP,
AVERAGE_SLEEP,
AVERAGE_SLEEP,
AVERAGE_SLEEP,
AVERAGE_SLEEP,
AVERAGE_SLEEP
};
memcpy(&s_data.metric_history[ActivityMetricSleepTotalSeconds], sleep_history,
sizeof(sleep_history));
}
// ---------------------------------------------------------------------------------------
// Test that the activity reward triggers when it should, and doesn't trigger when it shouldn't
void test_activity_insights__activity_reward_no_trigger_default_state(void) {
prv_set_step_history_avg();
activity_insights_init(rtc_get_time());
// Make sure we don't trigger in the default state (not above median, not active)
s_data.steps_per_minute = 0;
s_data.metric_history[ActivityMetricStepCount][0] = AVERAGE_STEPS;
prv_minute_update(ACTIVE_MINUTES);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
}
void test_activity_insights__activity_reward_no_trigger_below_avg(void) {
prv_set_step_history_avg();
activity_insights_init(rtc_get_time());
// Make sure that when we are active, we don't trigger without being above average
s_data.steps_per_minute = 80;
s_data.metric_history[ActivityMetricStepCount][0] = AVERAGE_STEPS;
prv_minute_update(ACTIVE_MINUTES);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
}
void test_activity_insights__activity_reward_no_trigger_not_active(void) {
prv_set_step_history_avg();
activity_insights_init(rtc_get_time());
// Make sure that being above average but not active doesn't trigger
s_data.steps_per_minute = 0;
s_data.metric_history[ActivityMetricStepCount][0] = HIGH_STEPS;
prv_minute_update(ACTIVE_MINUTES);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
}
void test_activity_insights__activity_reward_trigger(void) {
prv_set_step_history_avg();
activity_insights_init(rtc_get_time());
// This would trigger the insights if they weren't disabled
s_data.steps_per_minute = 80;
s_data.metric_history[ActivityMetricStepCount][0] = HIGH_STEPS;
prv_minute_update(ACTIVE_MINUTES);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
// // This tests multi day triggers if insights are enabled
// prv_minute_update(ACTIVE_MINUTES);
// cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
// rtc_set_time(rtc_get_time() + 1 * SECONDS_PER_DAY);
// activity_insights_recalculate_stats();
// prv_minute_update(ACTIVE_MINUTES);
// cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
}
void test_activity_insights__disable_activity_reward(void) {
prv_set_step_history_avg();
activity_insights_init(rtc_get_time());
// Set up criteria to trigger reward
s_data.steps_per_minute = 80;
s_data.metric_history[ActivityMetricStepCount][0] = HIGH_STEPS;
// Make sure setting enable to false actually disables things
activity_insights_recalculate_stats();
ActivityInsightSettings disabled_activity;
activity_insights_settings_read(ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_REWARD, &disabled_activity);
disabled_activity.enabled = false,
settings_file_set(NULL, /* fake settings file don't care */
ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_REWARD,
strlen(ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_REWARD), &disabled_activity,
sizeof(disabled_activity));
pfs_watch_cb(NULL); // Update the settings cache
prv_minute_update(ACTIVE_MINUTES);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
}
// ---------------------------------------------------------------------------------------
// Make sure we don't push an activity pin when we have no history to compare against
void test_activity_insights__activity_summary_no_history(void) {
// Tests init with zero history
// Set time to be after 8:30PM
rtc_set_time((20 * SECONDS_PER_HOUR) + (40 * SECONDS_PER_MINUTE));
activity_insights_init(rtc_get_time());
// Provide non-zero step count
s_data.metric_history[ActivityMetricStepCount][0] = 500;
activity_insights_process_minute_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 0);
}
// ---------------------------------------------------------------------------------------
void test_activity_insights__sleep_summary(void) {
// Use reasonable insight settings
prv_set_sleep_history_avg();
// Let's start at 11:30pm
struct tm start_tm = {
// Thursday, Jan 1, 2015, 11:30pm
.tm_hour = 23,
.tm_min = 30,
.tm_mday = 1,
.tm_mon = 0,
.tm_year = 115
};
prv_set_time(&start_tm);
activity_insights_init(rtc_get_time());
// Make sure we don't trigger while still asleep
s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateLightSleep;
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 0);
// Put in a 1 hour sleep session that ends at 11pm. This is after
// ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY (9pm), so it should be part of "tonight's" sleep.
prv_add_sleep_session(22, 1); // Starting 22 hours from midnight of today
// Awake until 11:45pm
rtc_set_time(rtc_get_time() + 15 * SECONDS_PER_MINUTE);
s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateAwake;
s_data.metric_history[ActivityMetricSleepStateSeconds][0] = 15 * SECONDS_PER_MINUTE;
// Should generate a pin
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 1);
Uuid orig_id = s_last_timeline_id;
// Advance to midnight and perform the midnight rollover logic
rtc_set_time(rtc_get_time() + 15 * SECONDS_PER_MINUTE); // Puts us at midnight
activity_insights_recalculate_stats(); // Process the midnight rollover logic
// Advance to 7:05am and add a sleep session from midnight to 7am
rtc_set_time(rtc_get_time() + 7 * SECONDS_PER_HOUR + 5 * SECONDS_PER_MINUTE); // Puts us at 7:05
prv_add_sleep_session(0, 7);
// Make sure we update the existing pin as soon as we are awake. We shouldn't add another pin
// because the sleep includes all sleep since ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY (9pm) the prior
// day.
s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateAwake;
s_data.metric_history[ActivityMetricSleepStateSeconds][0] = 5 * SECONDS_PER_MINUTE;
activity_insights_process_sleep_data(rtc_get_time());
// Pin added should have been called again, but with the same UUID
cl_assert_equal_i(s_data.pins_added, 2);
cl_assert(uuid_equal(&orig_id, &s_last_timeline_id));
// Make sure we don't trigger again for the same sleep session
s_data.metric_history[ActivityMetricSleepStateSeconds][0] = 60 * SECONDS_PER_MINUTE;
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 2);
}
// ---------------------------------------------------------------------------------------
// This makes sure the sleep summary properly handles the midnight rollover for non-UTC timezones
// since when there's no sleep, the enter/exit times will be set to midnight UTC and the metrics
// will return that time in localtime
void test_activity_insights__sleep_summary_midnight_timezone(void) {
// Set to a non-UTC timezone
static TimezoneInfo tz = {
.tm_gmtoff = -8 * SECONDS_PER_HOUR, // PST
};
time_util_update_timezone(&tz);
rtc_set_time(time_util_get_midnight_of(rtc_get_time()) + tz.tm_gmtoff);
prv_set_sleep_history_avg();
activity_insights_init(rtc_get_time());
// At midnight, enter/exit get set to midnight UTC (for PST, this is 4PM), total sleep is 0
uint32_t midnight_local = ((24 + tz.tm_gmtoff) % 24) * SECONDS_PER_HOUR;
s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateAwake;
s_data.metric_history[ActivityMetricSleepStateSeconds][0] = 30 * SECONDS_PER_MINUTE;
s_data.metric_history[ActivityMetricSleepExitAtSeconds][0] = midnight_local;
s_data.metric_history[ActivityMetricSleepEnterAtSeconds][0] = midnight_local;
s_data.metric_history[ActivityMetricSleepTotalSeconds][0] = 0;
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 0);
// Make sure pin is pushed once we have slept some
prv_add_sleep_session(0, 7);
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 1);
}
// ---------------------------------------------------------------------------------------
// Make sure that if you wake up for a short period of time, we'll move the sleep pin
void test_activity_insights__sleep_summary_merge(void) {
prv_set_sleep_history_avg();
activity_insights_init(rtc_get_time());
// Common metrics
s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateAwake;
s_data.metric_history[ActivityMetricSleepStateSeconds][0] = 30 * SECONDS_PER_MINUTE;
s_data.metric_history[ActivityMetricSleepEnterAtSeconds][0] = 0;
// First session should always produce a pin
prv_add_sleep_session(0, 7);
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 1);
Uuid orig_id = s_last_timeline_id;
// Next session, < 1h after should move the pin
rtc_set_time(rtc_get_time() + 2 * SECONDS_PER_HOUR);
prv_add_sleep_session(0.5, 1.5);
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 2);
cl_assert(uuid_equal(&orig_id, &s_last_timeline_id));
// Nap sessions shouldn't be added
rtc_set_time(rtc_get_time() + 3 * SECONDS_PER_HOUR);
prv_add_nap_session(2, 1);
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 2);
}
// ---------------------------------------------------------------------------------------
// Make sure that when the watch resets, we retain state properly
void test_activity_insights__sleep_summary_power_cycle(void) {
prv_set_sleep_history_avg();
activity_insights_init(rtc_get_time());
// Common metrics
s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateAwake;
s_data.metric_history[ActivityMetricSleepStateSeconds][0] = 30 * SECONDS_PER_MINUTE;
s_data.metric_history[ActivityMetricSleepEnterAtSeconds][0] = 0;
// Push a pin
rtc_set_time(rtc_get_time() + 5 * SECONDS_PER_HOUR);
prv_add_sleep_session(0, 7);
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 1);
// Re-init (simulates power cycle) and make sure we don't add a pin again
activity_insights_init(rtc_get_time());
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 1);
// Make sure we still merge properly after a power cycle
activity_insights_init(rtc_get_time());
rtc_set_time(rtc_get_time() + 2 * SECONDS_PER_HOUR);
prv_add_sleep_session(0.5, 1.5);
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 2);
}
// ---------------------------------------------------------------------------------------
// Make sure we don't push a pin when we have no history to compare against
void test_activity_insights__sleep_summary_no_history(void) {
// Tests init with zero history
activity_insights_init(rtc_get_time());
// Make sure we don't trigger as soon as we're awake
s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateAwake;
s_data.metric_history[ActivityMetricSleepExitAtSeconds][0] = 7 * SECONDS_PER_HOUR;
s_data.metric_history[ActivityMetricSleepStateSeconds][0] = 30 * SECONDS_PER_MINUTE;
activity_insights_process_sleep_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 0);
}
// ---------------------------------------------------------------------------------------
void test_activity_insights__activation_delay_insights_time_trigger(void) {
time_t now = mktime(&s_init_time_tm);
prv_set_activation_time(now);
activity_insights_process_minute_data(now);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
now += SECONDS_PER_DAY; // Jan 2 @ 10:00am
rtc_set_time(now);
activity_insights_process_minute_data(now);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
now += 8 * SECONDS_PER_HOUR; // Jan 2 @ 6:00pm
rtc_set_time(now);
activity_insights_process_minute_data(now);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
now += (3 * SECONDS_PER_DAY) + (2 * SECONDS_PER_HOUR); // Jan 5 @ 8:00pm
rtc_set_time(now);
activity_insights_process_minute_data(now);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
s_health_app_opened_version = 1;
now += 30 * SECONDS_PER_MINUTE; // Jan 5 @ 8:30pm
rtc_set_time(now);
activity_insights_process_minute_data(now);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 2);
now += 6 * SECONDS_PER_DAY; // Jan 11 @ 8:30pm
rtc_set_time(now);
activity_insights_process_minute_data(now);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 3);
}
// ---------------------------------------------------------------------------------------
void test_activity_insights__activation_delay_insights_fifteen_interval_trigger(void) {
time_t now = mktime(&s_init_time_tm);
prv_set_activation_time(now);
activity_insights_process_minute_data(now);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
now += SECONDS_PER_DAY + (8 * SECONDS_PER_HOUR) + (5 * SECONDS_PER_MINUTE); // Jan 2 @ 6:05pm
rtc_set_time(now);
activity_insights_process_minute_data(now);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
now += (10 * SECONDS_PER_MINUTE); // Jan 2 @ 6:15pm
rtc_set_time(now);
activity_insights_process_minute_data(now);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
}
// Make sure that when the watch resets, we retain state properly
void test_activity_insights__nap_session_power_cycle(void) {
// PBL-36355 Disable nap notifications
// Enable this unit test when re-enabling nap session notifications
#if 0
prv_set_sleep_history_avg();
activity_insights_init(rtc_get_time());
// Common metrics
s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateAwake;
s_data.metric_history[ActivityMetricSleepStateSeconds][0] = 30 * SECONDS_PER_MINUTE;
s_data.metric_history[ActivityMetricSleepEnterAtSeconds][0] = 0;
// Push a pin
rtc_set_time(rtc_get_time() + 5 * SECONDS_PER_HOUR);
prv_add_nap_session(0, 1);
activity_insights_process_minute_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 1);
// Re-init (simulates power cycle) and make sure we don't add a pin again
activity_insights_init(rtc_get_time());
activity_insights_process_minute_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 1);
// Make sure we still trigger properly after a power cycle
activity_insights_init(rtc_get_time());
rtc_set_time(rtc_get_time() + 2 * SECONDS_PER_HOUR);
prv_add_nap_session(0.5, 1.5);
activity_insights_process_minute_data(rtc_get_time());
cl_assert_equal_i(s_data.pins_added, 2);
#endif
}
// Make sure that when the watch resets, we retain state properly
void test_activity_insights__walk_session_power_cycle(void) {
activity_insights_init(rtc_get_time());
// Common metrics
s_data.metric_history[ActivityMetricSleepState][0] = ActivitySleepStateAwake;
s_data.metric_history[ActivityMetricSleepStateSeconds][0] = 30 * SECONDS_PER_MINUTE;
s_data.metric_history[ActivityMetricSleepEnterAtSeconds][0] = 0;
// Push a pin
rtc_set_time(rtc_get_time() + 5 * SECONDS_PER_HOUR);
prv_add_walk_session(0, 1);
rtc_set_time(rtc_get_time() + 2 * SECONDS_PER_HOUR);
activity_insights_process_minute_data(rtc_get_time());
cl_assert_equal_i(s_data.notifs_shown, 1);
// Re-init (simulates power cycle) and make sure we don't add a pin again
activity_insights_init(rtc_get_time());
activity_insights_process_minute_data(rtc_get_time());
cl_assert_equal_i(s_data.notifs_shown, 1);
// Make sure we still trigger properly after a power cycle
activity_insights_init(rtc_get_time());
prv_add_walk_session(0, 1);
rtc_set_time(rtc_get_time() + 2 * SECONDS_PER_HOUR);
activity_insights_process_minute_data(rtc_get_time());
cl_assert_equal_i(s_data.notifs_shown, 2);
}
@@ -0,0 +1,35 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "stubs_i18n.h"
#include "stubs_fonts.h"
#include "stubs_graphics.h"
#include "stubs_text_node.h"
#include "shell/prefs.h"
#include "services/normal/activity/health_util.h"
UnitsDistance shell_prefs_get_units_distance(void) {
return UnitsDistance_Miles;
}
void test_health_util__pace(void) {
cl_assert_equal_i(health_util_get_pace(29, 4800), 10); // PBL-36661
cl_assert_equal_i(health_util_get_pace(10, 800), 20); // less than a mile
cl_assert_equal_i(health_util_get_pace(820, 262400), 5); // many miles / long distance
}
+60
View File
@@ -0,0 +1,60 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "services/normal/activity/hr_util.h"
#include "stubs_activity.h"
void test_hr_util__initialize(void) {
}
void test_hr_util__cleanup(void) {
}
// ---------------------------------------------------------------------------------------
void test_hr_util__get_hr_zone(void) {
// Test some BPM values below the min_hr
cl_assert_equal_i(hr_util_get_hr_zone(20), HRZone_Zone0);
cl_assert_equal_i(hr_util_get_hr_zone(40), HRZone_Zone0);
cl_assert_equal_i(hr_util_get_hr_zone(60), HRZone_Zone0);
// Test some valid BPM values
cl_assert_equal_i(hr_util_get_hr_zone(80), HRZone_Zone0);
cl_assert_equal_i(hr_util_get_hr_zone(100), HRZone_Zone0);
cl_assert_equal_i(hr_util_get_hr_zone(120), HRZone_Zone0);
cl_assert_equal_i(hr_util_get_hr_zone(140), HRZone_Zone1);
cl_assert_equal_i(hr_util_get_hr_zone(160), HRZone_Zone2);
cl_assert_equal_i(hr_util_get_hr_zone(180), HRZone_Zone3);
// Test some BPM values above the max_hr
cl_assert_equal_i(hr_util_get_hr_zone(200), HRZone_Zone3);
cl_assert_equal_i(hr_util_get_hr_zone(220), HRZone_Zone3);
cl_assert_equal_i(hr_util_get_hr_zone(240), HRZone_Zone3);
}
void test_hr_util__is_elevated(void) {
cl_assert_equal_b(hr_util_is_elevated(60), false);
cl_assert_equal_b(hr_util_is_elevated(80), false);
cl_assert_equal_b(hr_util_is_elevated(99), false);
cl_assert_equal_b(hr_util_is_elevated(100), true);
cl_assert_equal_b(hr_util_is_elevated(120), true);
cl_assert_equal_b(hr_util_is_elevated(240), true);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,765 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "services/normal/activity/activity.h"
#include "services/normal/activity/activity_calculators.h"
#include "services/normal/activity/workout_service.h"
#include "services/common/hrm/hrm_manager.h"
#include "process_management/app_install_types.h"
#include "util/time/time.h"
#include "util/units.h"
// ---------------------------------------------------------------------------------------
#include "stubs_activity_insights.h"
#include "stubs_evented_timer.h"
#include "stubs_logging.h"
#include "stubs_pbl_malloc.h"
#include "stubs_regular_timer.h"
#include "fake_rtc.h"
#include "fake_mutex.h"
extern void prv_abandon_workout_timer_callback(void *data);
extern void prv_abandoned_notification_timer_callback(void *data);
extern void prv_workout_timer_cb(void *data);
extern bool workout_service_get_avg_hr(int32_t *avg_hr_out);
extern bool workout_service_get_current_workout_hr_zone_time(int32_t *hr_zone_time_s_out);
extern void workout_service_get_active_kcalories(int32_t *active);
extern void workout_service_reset(void);
// Stubs
///////////////////////////////////////////////////////////
static ActivitySession s_saved_session;
void activity_sessions_prv_add_activity_session(ActivitySession *session) {
s_saved_session = *session;
}
void activity_sessions_prv_delete_activity_session(ActivitySession *session) {}
void activity_algorithm_enable_activity_tracking(bool enable) {}
bool activity_get_sessions(uint32_t *session_entries, ActivitySession *sessions) {
return false;
}
uint8_t activity_prefs_get_age_years(void) {
return 30; // This is our current default
}
ActivityGender activity_prefs_get_gender(void) {
return ActivityGenderMale;
}
uint16_t activity_prefs_get_weight_dag(void) {
return 7539;
}
uint16_t activity_prefs_get_height_mm(void) {
return 1900;
}
uint8_t activity_prefs_heart_get_elevated_hr(void) {
return 100;
}
uint8_t activity_prefs_heart_get_zone1_threshold(void) {
return 130;
}
uint8_t activity_prefs_heart_get_zone2_threshold(void) {
return 154;
}
uint8_t activity_prefs_heart_get_zone3_threshold(void) {
return 172;
}
AppInstallId app_get_app_id(void) {
return 0;
}
// ---------------------------------------------------------------------------------------
static bool s_hrm_subscribed;
static uint32_t s_hrm_expiration;
HRMSessionRef sys_hrm_manager_app_subscribe(AppInstallId app_id, uint32_t update_interval_s,
uint16_t expire_s, HRMFeature features) {
s_hrm_subscribed = true;
s_hrm_expiration = expire_s;
return 1;
}
bool sys_hrm_manager_unsubscribe(HRMSessionRef ref) {
s_hrm_subscribed = false;
s_hrm_expiration = 0;
return true;
}
uint32_t time_get_uptime_seconds(void) {
return SECONDS_PER_DAY + rtc_get_time();
}
static uint32_t s_total_step_count;
bool activity_get_metric(ActivityMetric metric, uint32_t history_len, int32_t *history) {
if (metric != ActivityMetricStepCount) {
return false;
}
*history = s_total_step_count;
return true;
}
static bool s_abandoned_workout_notification_sent;
void workout_utils_send_abandoned_workout_notification() {
s_abandoned_workout_notification_sent = true;
}
// ---------------------------------------------------------------------------------------
static void prv_inc_steps_and_put_event(int steps) {
s_total_step_count += steps;
PebbleEvent event = {
.type = PEBBLE_HEALTH_SERVICE_EVENT,
.health_event = {
.type = HealthEventMovementUpdate,
.data.movement_update.steps = s_total_step_count,
},
};
event_put(&event);
workout_service_health_event_handler(&event.health_event);
}
// ---------------------------------------------------------------------------------------
static void prv_put_bpm_event(int bpm, HRMQuality quality) {
PebbleEvent event = {
.type = PEBBLE_HEALTH_SERVICE_EVENT,
.health_event = {
.type = HealthEventHeartRateUpdate,
.data.heart_rate_update.current_bpm = bpm,
.data.heart_rate_update.quality = quality,
},
};
event_put(&event);
workout_service_health_event_handler(&event.health_event);
}
static void prv_inc_time(int seconds) {
fake_rtc_increment_time(seconds);
prv_workout_timer_cb(NULL);
}
// ---------------------------------------------------------------------------------------
void test_workout_service__initialize(void) {
fake_rtc_init(0, 0);
workout_service_reset();
// Start the step value for the day at X
s_total_step_count = 5000;
s_hrm_expiration = 0;
s_hrm_subscribed = false;
s_abandoned_workout_notification_sent = false;
const bool assert_all_unlocked = true;
fake_mutex_reset(assert_all_unlocked);
workout_service_init();
}
void test_workout_service__cleanup(void) {
workout_service_stop_workout();
const bool assert_all_unlocked = true;
fake_mutex_reset(assert_all_unlocked);
}
// ---------------------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------------------
void test_workout_service__basic(void) {
cl_assert(workout_service_start_workout(ActivitySessionType_Run));
int32_t steps, duration_s, distance_m, bpm;
HRZone hr_zone;
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 0);
cl_assert_equal_i(duration_s, 0);
cl_assert_equal_i(distance_m, 0);
cl_assert_equal_i(bpm, 0);
cl_assert_equal_i(hr_zone, 0);
// Get some step info
prv_inc_time(5 * SECONDS_PER_MINUTE);
prv_inc_steps_and_put_event(900 /* 180 steps per min * 5 mins */);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 900);
cl_assert_equal_i(5900, s_total_step_count);
cl_assert_equal_i(distance_m, 1201 /* 1.2km in 5 mins is reasonable */);
cl_assert_equal_i(duration_s, 5 * SECONDS_PER_MINUTE);
// Get some HR info
prv_put_bpm_event(100, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(bpm, 100);
cl_assert_equal_i(hr_zone, 0);
// Get some more step info
prv_inc_time(5 * SECONDS_PER_MINUTE);
prv_inc_steps_and_put_event(900 /* 180 steps per min * 5 mins */);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 1800);
cl_assert_equal_i(6800, s_total_step_count);
cl_assert_equal_i(distance_m, 2402 /* 2.4km in 10 mins is reasonable */);
cl_assert_equal_i(duration_s, 10 * SECONDS_PER_MINUTE);
// Get some more HR info
prv_inc_time(10);
prv_put_bpm_event(180, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(bpm, 180);
cl_assert_equal_i(hr_zone, 3);
cl_assert_equal_i(duration_s, 10 * SECONDS_PER_MINUTE + 10);
cl_assert(workout_service_stop_workout());
cl_assert(!workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
}
// ---------------------------------------------------------------------------------------
void test_workout_service__takeover_activity_session(void) {
int32_t steps, duration_s, distance_m, bpm, active_kcalories;
HRZone hr_zone;
// We have a session that started at utc=10 and length_min of 10. Increment the time
// so that makes sense
prv_inc_time(610);
// Start a new session
ActivitySession session = {
.start_utc = 10,
.length_min = 10,
.type = ActivitySessionType_Run,
.ongoing = true,
.manual = false,
.step_data = {
.steps = 2000,
.active_kcalories = 200,
.resting_kcalories = 100,
.distance_meters = 1600,
},
};
cl_assert(workout_service_takeover_activity_session(&session));
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 2000);
cl_assert_equal_i(duration_s, 600);
cl_assert_equal_i(distance_m, 1600);
// Get the active calories, should be same as session
workout_service_get_active_kcalories(&active_kcalories);
cl_assert_equal_i(200, active_kcalories);
// Add some time and steps
prv_inc_time(600); /* 10 mins */
prv_inc_steps_and_put_event(1800 /* 180 steps per min * 10 mins */);
// Grab the new distance and active_kcalories
int32_t new_active_kcalories, new_distance_m;
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &new_distance_m,
&bpm, &hr_zone));
// Compute our own version of active_kcalories
const int32_t distance_delta_m = (new_distance_m - distance_m);
const int32_t calculated_active_kcalories
= ROUND(activity_private_compute_active_calories(distance_delta_m * MM_PER_METER, 600 * MS_PER_SECOND),
ACTIVITY_CALORIES_PER_KCAL);
// Make sure that the new_active_kcalories has increased
workout_service_get_active_kcalories(&new_active_kcalories);
cl_assert(active_kcalories < new_active_kcalories);
// Double check that we calculated the same value that the workout service got
cl_assert_equal_i(calculated_active_kcalories, new_active_kcalories - active_kcalories);
workout_service_stop_workout();
// Pull out the saved session and make sure it matches our numbers
ActivitySession *stored_session = &s_saved_session;
cl_assert_equal_i(stored_session->start_utc, session.start_utc);
cl_assert_equal_i(stored_session->length_min, 20);
cl_assert_equal_i(stored_session->step_data.steps, 3800);
cl_assert_equal_i(stored_session->step_data.active_kcalories, 200 + calculated_active_kcalories);
cl_assert_equal_i(stored_session->step_data.resting_kcalories,
ROUND(activity_private_compute_resting_calories(stored_session->length_min),
ACTIVITY_CALORIES_PER_KCAL));
cl_assert_equal_b(stored_session->manual, true);
cl_assert(stored_session->step_data.distance_meters > 2000);
}
// ---------------------------------------------------------------------------------------
void test_workout_service__pause_resume(void) {
int32_t steps, duration_s, distance_m, bpm;
HRZone hr_zone;
cl_assert(workout_service_start_workout(ActivitySessionType_Run));
prv_inc_time(10);
prv_inc_steps_and_put_event(10);
prv_put_bpm_event(100, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 10);
cl_assert_equal_i(duration_s, 10);
cl_assert_equal_i(bpm, 100);
cl_assert(workout_service_pause_workout(true));
// Get some new data but out stats shouldn't change (except HR)
prv_inc_time(10);
prv_inc_steps_and_put_event(10);
prv_put_bpm_event(110, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 10);
cl_assert_equal_i(duration_s, 10);
cl_assert_equal_i(bpm, 110);
// Get some more new data and out stats still shouldn't change (except HR)
prv_inc_time(10);
prv_inc_steps_and_put_event(10);
prv_put_bpm_event(190, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 10);
cl_assert_equal_i(duration_s, 10);
cl_assert_equal_i(bpm, 190);
// Resume the workout and get more data (it updates this time)
cl_assert(workout_service_pause_workout(false));
prv_inc_time(10);
prv_inc_steps_and_put_event(10);
prv_put_bpm_event(80, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 20);
cl_assert_equal_i(duration_s, 20);
cl_assert_equal_i(bpm, 80);
cl_assert(workout_service_pause_workout(true));
cl_assert(workout_service_pause_workout(true));
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 20);
cl_assert_equal_i(duration_s, 20);
cl_assert_equal_i(bpm, 80);
prv_inc_time(10);
prv_inc_steps_and_put_event(10);
prv_put_bpm_event(117, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 20);
cl_assert_equal_i(duration_s, 20);
cl_assert_equal_i(bpm, 117);
cl_assert(workout_service_pause_workout(false));
cl_assert(workout_service_pause_workout(false));
prv_inc_time(10);
prv_inc_steps_and_put_event(10);
prv_put_bpm_event(113, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 30);
cl_assert_equal_i(duration_s, 30);
cl_assert_equal_i(bpm, 113);
cl_assert(workout_service_stop_workout());
}
// ---------------------------------------------------------------------------------------
void test_workout_service__expire_hr_reading(void) {
int32_t steps, duration_s, distance_m, bpm;
HRZone hr_zone;
cl_assert(workout_service_start_workout(ActivitySessionType_Run));
prv_put_bpm_event(100, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(bpm, 100);
// Time forward X seconds
prv_inc_time(30);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(bpm, 100);
// Time forward X seconds
prv_inc_time(30);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
// HR Reading has expired. Should return 0
cl_assert_equal_i(bpm, 0);
cl_assert(workout_service_stop_workout());
}
// ---------------------------------------------------------------------------------------
void test_workout_service__receive_offwrist_reading(void) {
int32_t steps, duration_s, distance_m, bpm;
HRZone hr_zone;
cl_assert(workout_service_start_workout(ActivitySessionType_Run));
// Put a good quality reading. Verify it was accepted.
prv_put_bpm_event(100, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(bpm, 100);
// Put an OffWrist reading. Verify we received it.
prv_put_bpm_event(50, HRMQuality_OffWrist);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(bpm, 0);
cl_assert_equal_i(hr_zone, HRZone_Zone0);
// Put a good quality reading. Verify it was accepted.
prv_put_bpm_event(100, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(bpm, 100);
cl_assert(workout_service_stop_workout());
}
// ---------------------------------------------------------------------------------------
void test_workout_service__working_out_past_midnight(void) {
int32_t steps, duration_s, distance_m, bpm;
HRZone hr_zone;
cl_assert(workout_service_start_workout(ActivitySessionType_Run));
// Start with only 10 steps and make sure it updates.
prv_inc_steps_and_put_event(10);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 10);
// Increment a lot so we can make sure that the wrap around of midnight works.
prv_inc_steps_and_put_event(1000);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 1010);
// Pretend we wrap around midnight. Midnight will reset the step count.
s_total_step_count = 0;
// Put some steps and make sure we don't delete or freak out, and append the new step count.
prv_inc_steps_and_put_event(50);
cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
&bpm, &hr_zone));
cl_assert_equal_i(steps, 1060);
}
// ---------------------------------------------------------------------------------------
// Open the app and close the app. Make sure the HR monitor turns off instantly
void test_workout_service__app_open_close_no_workout(void) {
// Put some time into the clock
prv_inc_time(1 * SECONDS_PER_MINUTE);
// Open the app, confirm that we are now subscribed with no end in sight
workout_service_frontend_opened();
cl_assert_equal_b(s_hrm_subscribed, true);
cl_assert_equal_i(s_hrm_expiration, 0);
workout_service_frontend_closed();
cl_assert_equal_b(s_hrm_subscribed, false);
cl_assert_equal_i(s_hrm_expiration, 0);
}
// ---------------------------------------------------------------------------------------
// Open the app, start a workout, close the app. Make sure the HR stays on for
// WORKOUT_ACTIVE_HR_SUBSCRIPTION_TS_EXPIRE
void test_workout_service__app_open_close_active_workout(void) {
// Put some time into the clock
prv_inc_time(1 * SECONDS_PER_MINUTE);
// Open the app, confirm that we are now subscribed with no end in sight
workout_service_frontend_opened();
cl_assert_equal_b(s_hrm_subscribed, true);
cl_assert_equal_i(s_hrm_expiration, 0);
cl_assert(workout_service_start_workout(ActivitySessionType_Run));
workout_service_frontend_closed();
cl_assert_equal_b(s_hrm_subscribed, true);
cl_assert_equal_i(s_hrm_expiration, SECONDS_PER_HOUR);
}
// ---------------------------------------------------------------------------------------
// Open the app, start a workout, 30s, stop the workout. Make sure the HR turns off instantly
// since workout wasn't valid (too short)
void test_workout_service__app_open_close_ended_invalid_workout(void) {
// Put some time into the clock
prv_inc_time(1 * SECONDS_PER_MINUTE);
// Open the app, confirm that we are now subscribed with no end in sight
workout_service_frontend_opened();
cl_assert_equal_b(s_hrm_subscribed, true);
cl_assert_equal_i(s_hrm_expiration, 0);
prv_inc_time(1 * SECONDS_PER_MINUTE);
cl_assert(workout_service_start_workout(ActivitySessionType_Run));
// Workout of 30 seconds duration. Should be invalid, not keep HR on after leaving
prv_inc_time(30);
cl_assert(workout_service_stop_workout());
workout_service_frontend_closed();
cl_assert_equal_b(s_hrm_subscribed, false);
cl_assert_equal_i(s_hrm_expiration, 0);
}
// ---------------------------------------------------------------------------------------
// Open the app, start a workout, 120s, stop the workout, 120s, close app.
// Make sure the HR stays on for WORKOUT_ENDED_HR_SUBSCRIPTION_TS_EXPIRE - 120s
void test_workout_service__app_open_close_valid_workout(void) {
// Put some time into the clock
prv_inc_time(1 * SECONDS_PER_MINUTE);
// Open the app, confirm that we are now subscribed with no end in sight
workout_service_frontend_opened();
cl_assert_equal_b(s_hrm_subscribed, true);
cl_assert_equal_i(s_hrm_expiration, 0);
prv_inc_time(1 * SECONDS_PER_MINUTE);
cl_assert(workout_service_start_workout(ActivitySessionType_Run));
// Workout of 120 seconds duration. Should be valid
prv_inc_time(2 * SECONDS_PER_MINUTE);
cl_assert(workout_service_stop_workout());
prv_inc_time(2 * SECONDS_PER_MINUTE);
workout_service_frontend_closed();
cl_assert_equal_b(s_hrm_subscribed, true);
cl_assert_equal_i(s_hrm_expiration, 8 * SECONDS_PER_MINUTE);
}
// ---------------------------------------------------------------------------------------
// Open the app, start a workout, 120s, stop the workout, 20 min, close app.
// Make sure the HR turns off right after we leave the app.
void test_workout_service__app_open_wait_close_valid_workout(void) {
// Put some time into the clock
prv_inc_time(1 * SECONDS_PER_MINUTE);
// Open the app, confirm that we are now subscribed with no end in sight
workout_service_frontend_opened();
cl_assert_equal_b(s_hrm_subscribed, true);
cl_assert_equal_i(s_hrm_expiration, 0);
prv_inc_time(1 * SECONDS_PER_MINUTE);
cl_assert(workout_service_start_workout(ActivitySessionType_Run));
// Workout of 120 seconds duration. Should be valid
prv_inc_time(2 * SECONDS_PER_MINUTE);
cl_assert(workout_service_stop_workout());
// Wait 20 minutes. By this time, as soon as we leave the app, we should turn off the HR device.
prv_inc_time(20 * SECONDS_PER_MINUTE);
workout_service_frontend_closed();
cl_assert_equal_b(s_hrm_subscribed, false);
cl_assert_equal_i(s_hrm_expiration, 0);
}
// ---------------------------------------------------------------------------------------
void test_workout_service__heart_rate_zone_time(void) {
const int ZONE_0_HR = 100;
const int ZONE_1_HR = 140;
const int ZONE_2_HR = 160;
const int ZONE_3_HR = 180;
int32_t hr_zone_time_s[HRZoneCount];
cl_assert(workout_service_start_workout(ActivitySessionType_Run));
cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 0);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 0);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 0);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 0);
prv_put_bpm_event(ZONE_1_HR, HRMQuality_Good);
prv_inc_time(10);
prv_put_bpm_event(ZONE_1_HR, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 0);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 0);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 0);
prv_inc_time(10);
prv_put_bpm_event(ZONE_2_HR, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 0);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 0);
prv_inc_time(10);
prv_put_bpm_event(ZONE_3_HR, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 0);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 10);
prv_inc_time(10);
prv_put_bpm_event(ZONE_0_HR, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 10);
prv_inc_time(10);
prv_put_bpm_event(ZONE_1_HR, HRMQuality_Good);
workout_service_get_current_workout_hr_zone_time(hr_zone_time_s);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 20);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 10);
// Pause the workout. Don't accumulate time in zones
cl_assert(workout_service_pause_workout(true));
prv_inc_time(10);
prv_put_bpm_event(ZONE_3_HR, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 20);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 10);
prv_inc_time(10);
prv_put_bpm_event(ZONE_1_HR, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 20);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 10);
// Unpause workout
cl_assert(workout_service_pause_workout(false));
prv_inc_time(10);
prv_put_bpm_event(ZONE_3_HR, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 20);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 20);
prv_inc_time(10);
prv_put_bpm_event(ZONE_2_HR, HRMQuality_Good);
cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 10);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 20);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 20);
cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 20);
cl_assert(!workout_service_get_current_workout_hr_zone_time(NULL));
cl_assert(workout_service_stop_workout());
cl_assert(!workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
}
// ---------------------------------------------------------------------------------------
void test_workout_service__avg_hr(void) {
int32_t avg_hr;
cl_assert(!workout_service_get_avg_hr(&avg_hr));
cl_assert(workout_service_start_workout(ActivitySessionType_Run));
prv_put_bpm_event(140, HRMQuality_Good);
prv_put_bpm_event(140, HRMQuality_Good);
cl_assert(workout_service_get_avg_hr(&avg_hr));
cl_assert_equal_i(avg_hr, 140);
prv_put_bpm_event(160, HRMQuality_Good);
prv_put_bpm_event(160, HRMQuality_Good);
cl_assert(workout_service_get_avg_hr(&avg_hr));
cl_assert_equal_i(avg_hr, 150);
cl_assert(workout_service_pause_workout(true));
prv_put_bpm_event(200, HRMQuality_Good);
prv_put_bpm_event(200, HRMQuality_Good);
cl_assert(workout_service_get_avg_hr(&avg_hr));
cl_assert_equal_i(avg_hr, 150);
cl_assert(workout_service_pause_workout(false));
prv_put_bpm_event(180, HRMQuality_Good);
prv_put_bpm_event(180, HRMQuality_Good);
cl_assert(workout_service_get_avg_hr(&avg_hr));
cl_assert_equal_i(avg_hr, 160);
}
// ---------------------------------------------------------------------------------------
// Open the app, start a workout, close app.
// 55 min - Make sure the notification was sent.
// 60 min - Make sure the workout was ended.
void test_workout_service__abandon_workout(void) {
// Put some time into the clock
prv_inc_time(1 * SECONDS_PER_MINUTE);
// Open the app and wait a minute
workout_service_frontend_opened();
prv_inc_time(1 * SECONDS_PER_MINUTE);
// Start workout and workout for 5 minutes
cl_assert(workout_service_start_workout(ActivitySessionType_Run));
prv_inc_time(5 * SECONDS_PER_MINUTE);
// Close app and wait 30 minutes
workout_service_frontend_closed();
prv_inc_time(30 * SECONDS_PER_MINUTE);
// Make sure notification is not sent yet and the workout is still ongoing
cl_assert_equal_b(s_abandoned_workout_notification_sent, false);
cl_assert_equal_b(workout_service_is_workout_ongoing(), true);
// Wait 25 minutes, call evented timer callback and make sure the notification is sent
prv_inc_time(25 * SECONDS_PER_MINUTE);
prv_abandoned_notification_timer_callback(NULL);
cl_assert_equal_b(s_abandoned_workout_notification_sent, true);
// Wait 5 minutes, call evented timer callback and make sure the workout was ended
prv_inc_time(5 * SECONDS_PER_MINUTE);
prv_abandon_workout_timer_callback(NULL);
cl_assert_equal_b(workout_service_is_workout_ongoing(), false);
}
+122
View File
@@ -0,0 +1,122 @@
from waftools.pebble_test import clar
def build(ctx):
clar(ctx,
sources_ant_glob = \
" src/fw/applib/health_service.c" \
" src/fw/flash_region/flash_region.c" \
" src/fw/flash_region/filesystem_regions.c" \
" src/fw/util/base64.c" \
" src/fw/util/stats.c" \
" src/fw/util/time/time.c" \
" src/fw/util/time/mktime.c" \
" src/fw/services/common/regular_timer.c " \
" src/fw/services/normal/activity/activity.c" \
" src/fw/services/normal/activity/activity_metrics.c" \
" src/fw/services/normal/activity/activity_sessions.c" \
" src/fw/services/normal/activity/activity_calculators.c" \
" src/fw/services/normal/activity/hr_util.c" \
" src/fw/services/normal/filesystem/flash_translation.c" \
" src/fw/services/normal/filesystem/pfs.c" \
" src/fw/services/normal/settings/settings_file.c" \
" src/fw/services/normal/settings/settings_raw_iter.c" \
" src/fw/shell/normal/prefs.c" \
" tests/fakes/fake_accel_service.c" \
" src/fw/util/crc8.c" \
" src/fw/util/legacy_checksum.c" \
" tests/fakes/fake_events.c" \
" tests/fakes/fake_rtc.c" \
" tests/fakes/fake_spi_flash.c",
test_sources_ant_glob = "test_activity.c",
override_includes=['dummy_board'],
defines=["DUMA_DISABLED", "CAPABILITY_HAS_HEALTH_TRACKING=1",
"CAPABILITY_HAS_BUILTIN_HRM=1"])
clar(ctx,
sources_ant_glob = \
" src/fw/util/base64.c" \
" src/fw/util/crc8.c" \
" src/fw/util/stats.c" \
" src/fw/util/time/time.c" \
" src/fw/util/time/mktime.c" \
" src/fw/services/normal/activity/activity_insights.c" \
" src/fw/services/normal/activity/insights_settings.c" \
" src/fw/services/normal/timeline/metricgroup.c" \
" src/fw/util/rand/rand.c" \
" src/fw/vendor/tinymt32/tinymt32.c" \
" tests/fakes/fake_clock.c" \
" tests/fakes/fake_settings_file.c" \
" tests/fakes/fake_kernel_services_notifications.c" \
" tests/fakes/fake_rtc.c",
test_sources_ant_glob = "test_activity_insights.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = \
" src/fw/services/normal/activity/kraepelin/kraepelin_algorithm.c" \
" tests/fakes/fake_rtc.c" \
" tests/fw/services/activity/step_samples.c" \
" tests/fw/services/activity/sleep_samples_v1.c" \
" tests/fw/services/activity/kraepelin_reference/fourier.c" \
" tests/fw/services/activity/kraepelin_reference/helper_worker.c" \
" tests/fw/services/activity/kraepelin_reference/ProjectK_worker.c" \
" tests/fw/services/activity/kraepelin_reference/raw_stats.c",
defines=['DUMA_DISABLED'], # DUMA false-positive
test_sources_ant_glob = "test_kraepelin_algorithm.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = \
" src/fw/flash_region/flash_region.c" \
" src/fw/flash_region/filesystem_regions.c" \
" src/fw/util/base64.c" \
" src/fw/util/crc8.c" \
" src/fw/util/shared_circular_buffer.c" \
" src/fw/util/time/time.c" \
" src/fw/util/time/mktime.c" \
" src/fw/services/common/regular_timer.c " \
" src/fw/services/normal/activity/kraepelin/activity_algorithm_kraepelin.c" \
" src/fw/services/normal/filesystem/flash_translation.c" \
" src/fw/services/normal/filesystem/pfs.c" \
" src/fw/services/normal/settings/settings_file.c" \
" src/fw/services/normal/settings/settings_raw_iter.c" \
" tests/fakes/fake_accel_service.c" \
" src/fw/util/legacy_checksum.c" \
" tests/fakes/fake_events.c" \
" tests/fakes/fake_rtc.c" \
" tests/fakes/fake_spi_flash.c",
test_sources_ant_glob = "test_activity_algorithm_kraepelin.c",
defines=['DUMA_DISABLED'], # DUMA false-positive, therefore disabled
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = \
" src/fw/services/normal/activity/health_util.c",
test_sources_ant_glob = "test_health_util.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = \
" src/fw/services/normal/activity/hr_util.c",
test_sources_ant_glob = "test_hr_util.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = \
" src/fw/services/normal/activity/activity_calculators.c" \
" src/fw/services/normal/activity/hr_util.c" \
" src/fw/services/normal/activity/workout_service.c" \
" tests/fakes/fake_events.c" \
" tests/fakes/fake_rtc.c",
test_sources_ant_glob = "test_workout_service.c",
defines=["CAPABILITY_HAS_BUILTIN_HRM=1"],
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = \
" src/fw/services/normal/activity/activity_calculators.c",
test_sources_ant_glob = "test_activity_calculators.c",
override_includes=['dummy_board'])
# vim:filetype=python