pebble/tests/fw/services/activity/test_activity_algorithm_kraepelin.c
2025-01-27 11:38:16 -08:00

960 lines
36 KiB
C

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