/* * 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 #include #include #include #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); } }