mirror of
https://github.com/google/pebble.git
synced 2025-03-28 05:47:46 +00:00
2223 lines
74 KiB
C
2223 lines
74 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 <stdint.h>
|
|
#include <string.h>
|
|
|
|
#include "applib/accel_service.h"
|
|
#include "services/common/hrm/hrm_manager_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 "system/logging.h"
|
|
#include "system/passert.h"
|
|
#include "util/list.h"
|
|
#include "util/math.h"
|
|
#include "util/size.h"
|
|
#include "util/time/time.h"
|
|
|
|
#include "clar.h"
|
|
|
|
// Stubs
|
|
#include "stubs_hexdump.h"
|
|
#include "stubs_logging.h"
|
|
#include "stubs_passert.h"
|
|
#include "stubs_pbl_malloc.h"
|
|
#include "stubs_prompt.h"
|
|
#include "stubs_sleep.h"
|
|
|
|
// Fakes
|
|
#include "fake_rtc.h"
|
|
|
|
HRMSessionRef s_hrm_next_session_ref = 1;
|
|
HRMSessionRef hrm_manager_subscribe_with_callback(AppInstallId app_id, uint32_t update_interval_s,
|
|
uint16_t expire_s, HRMFeature features,
|
|
HRMSubscriberCallback callback, void *context) {
|
|
return s_hrm_next_session_ref++;
|
|
}
|
|
|
|
bool sys_hrm_manager_unsubscribe(HRMSessionRef session) {
|
|
cl_assert(session < s_hrm_next_session_ref);
|
|
return true;
|
|
}
|
|
|
|
#include <dirent.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
extern char *strdup(const char *s);
|
|
|
|
// Define this to save stats to a file at the end of unit tests
|
|
// #define STATS_FILE_NAME "/Users/ronmarianetti/Temp/stats.csv"
|
|
|
|
// Define this to run only 1 of the step tests
|
|
// #define STEP_TEST_ONLY "walk_100_pbl_25296_1"
|
|
|
|
// Define this to run only 1 of the step tests
|
|
// #define SLEEP_TEST_ONLY "sleep_1_end_deep"
|
|
|
|
// Define this to run only 1 of the activity tests
|
|
// #define ACTIVITY_TEST_ONLY "walk_activities_0"
|
|
|
|
// Implemented in activity/step_samples.c
|
|
AccelRawData *activity_sample_30_steps(int *len);
|
|
AccelRawData *activity_sample_working_at_desk(int *len);
|
|
AccelRawData *activity_sample_not_moving(int *len);
|
|
|
|
// Implemented in activity/sleep_samples_v1.c
|
|
AlgMinuteFileSampleV5 *activity_sample_sleep_v1_1(int *len);
|
|
|
|
typedef AccelRawData *AccelRawDataPtr;
|
|
typedef AccelRawDataPtr (*ActivitySamplesFunc)(int *len);
|
|
|
|
// These are the values we capture and compare against for every minute of accel data
|
|
typedef struct {
|
|
uint8_t steps; // # of steps in this minute
|
|
uint8_t orientation; // average orientation of the watch
|
|
uint16_t vmc; // VMC (Vector Magnitude Counts) for this minute
|
|
} TestMinuteData;
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Structure holding the samples and expected values for a step sample discovered on the file
|
|
// system
|
|
typedef struct {
|
|
char name[256];
|
|
AccelRawData *samples;
|
|
int num_samples;
|
|
int exp_steps;
|
|
int exp_steps_min;
|
|
int exp_steps_max;
|
|
float weight; // Weight percent error by this factor
|
|
int test_idx; // used after we run the test, for sorting
|
|
} StepFileTestEntry;
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Array of samples and expected results for sleep tests
|
|
typedef AlgMinuteFileSample *AlgSleepMinuteDataPtr;
|
|
typedef AlgSleepMinuteDataPtr (*ActivityMinuteFunc)(int *len);
|
|
typedef struct {
|
|
int value;
|
|
int min;
|
|
int max;
|
|
} ExpectedValue;
|
|
|
|
typedef struct {
|
|
int value;
|
|
bool passed;
|
|
} ActualValue;
|
|
|
|
typedef struct {
|
|
char name[256];
|
|
AlgMinuteFileSample *samples;
|
|
int num_samples;
|
|
int version;
|
|
|
|
ExpectedValue total;
|
|
ExpectedValue deep;
|
|
ExpectedValue start_at;
|
|
ExpectedValue end_at;
|
|
ExpectedValue cur_state_elapsed;
|
|
ExpectedValue in_sleep;
|
|
ExpectedValue in_deep_sleep;
|
|
|
|
float weight; // Weight percent error by this factor
|
|
int test_idx;
|
|
int force_shut_down_at;
|
|
} SleepFileTestEntry;
|
|
|
|
typedef struct {
|
|
ActualValue total;
|
|
ActualValue deep;
|
|
ActualValue start_at;
|
|
ActualValue end_at;
|
|
ActualValue cur_state_elapsed;
|
|
ActualValue in_sleep;
|
|
ActualValue in_deep_sleep;
|
|
|
|
float weighted_err;
|
|
bool all_passed;
|
|
} SleepTestResults;
|
|
|
|
|
|
typedef struct {
|
|
char name[256];
|
|
AlgMinuteFileSample *samples;
|
|
int num_samples;
|
|
int version;
|
|
|
|
ExpectedValue activity_type;
|
|
ExpectedValue len;
|
|
ExpectedValue start_at;
|
|
|
|
float weight; // Weight percent error by this factor
|
|
int test_idx;
|
|
int force_shut_down_at;
|
|
} ActivityFileTestEntry;
|
|
|
|
typedef struct {
|
|
ActualValue activity_type;
|
|
ActualValue len;
|
|
ActualValue start_at;
|
|
|
|
float weighted_err;
|
|
bool all_passed;
|
|
} ActivityTestResults;
|
|
|
|
|
|
typedef struct {
|
|
KAlgActivityType activity;
|
|
time_t start_utc;
|
|
uint16_t len_minutes;
|
|
bool ongoing;
|
|
uint16_t steps;
|
|
uint32_t resting_calories;
|
|
uint32_t active_calories;
|
|
uint32_t distance_mm;
|
|
} KAlgTestActivitySession;
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Globals
|
|
void *s_kalg_state;
|
|
|
|
|
|
// ==================================================================================
|
|
// Assertion utilities
|
|
// Assert that a particular activity session is present in the sessions list
|
|
static void prv_assert_activity_present(KAlgTestActivitySession *sessions, int num_sessions,
|
|
KAlgTestActivitySession *exp_session,
|
|
char *file, int line) {
|
|
for (int i = 0; i < num_sessions; i++) {
|
|
if (sessions[i].activity == exp_session->activity
|
|
&& sessions[i].start_utc == exp_session->start_utc
|
|
&& sessions[i].len_minutes == exp_session->len_minutes
|
|
&& sessions[i].active_calories == exp_session->active_calories
|
|
&& sessions[i].resting_calories == exp_session->resting_calories
|
|
&& sessions[i].steps == exp_session->steps) {
|
|
return;
|
|
}
|
|
}
|
|
printf("\nFound activities:");
|
|
for (int i = 0; i < num_sessions; i++) {
|
|
printf("\nFound: type: %d, start_utc: %d, len: %"PRIu16", steps: %"PRIu16", "
|
|
"rest_cal: %"PRIu32", active_cal: %"PRIu32", dist: %"PRIu32" ",
|
|
(int)sessions[i].activity, (int)sessions[i].start_utc, sessions[i].len_minutes,
|
|
sessions[i].steps, sessions[i].resting_calories, sessions[i].active_calories,
|
|
sessions[i].distance_mm);
|
|
}
|
|
printf("\nLooking for: type: %d, start_utc: %d, len: %"PRIu16", steps: %"PRIu16", "
|
|
"rest_cal: %"PRIu32", active_cal: %"PRIu32", dist: %"PRIu32" ",
|
|
(int)exp_session->activity, (int)exp_session->start_utc,
|
|
exp_session->len_minutes, exp_session->steps, exp_session->resting_calories,
|
|
exp_session->active_calories, exp_session->distance_mm);
|
|
clar__assert(false, file, line, "Missing activity record", "", true);
|
|
}
|
|
|
|
#define ASSERT_ACTIVITY_SESSION_PRESENT(sessions, num_sessions, session) \
|
|
prv_assert_activity_present((sessions), (num_sessions), (session), __FILE__, __LINE__)
|
|
|
|
|
|
|
|
// ==================================================================================
|
|
// Functions used for collecting stats and writing them out to a csv
|
|
static const int k_stats_max_columns = 32;
|
|
typedef struct {
|
|
ListNode node;
|
|
uint32_t values[k_stats_max_columns];
|
|
} StatsRow;
|
|
typedef enum {
|
|
StatsEpochTypeNonStepping = 0,
|
|
StatsEpochTypePartialStepping = 1, // First or last epoch in a test
|
|
StatsEpochTypeStepping = 2, // First or last epoch in a test
|
|
} StatsEpochType;
|
|
|
|
static int s_stats_num_columns = 0;
|
|
static char *s_stats_column_names[k_stats_max_columns];
|
|
static StatsRow *s_stat_rows;
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
static void prv_stats_reinit(void) {
|
|
// Delete stuff from prior stats run
|
|
cl_assert(s_stats_num_columns < k_stats_max_columns);
|
|
for (int i = 0; i < s_stats_num_columns; i++) {
|
|
free(s_stats_column_names[i]);
|
|
s_stats_column_names[i] = NULL;
|
|
}
|
|
|
|
// Free the rows
|
|
StatsRow *next = s_stat_rows;
|
|
while (next) {
|
|
StatsRow *to_free = next;
|
|
next = (StatsRow *)next->node.next;
|
|
free(to_free);
|
|
}
|
|
s_stat_rows = NULL;
|
|
s_stats_num_columns = 0;
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Callback called by the algorithm. This collects stats from an epoch
|
|
static void prv_stats_cb(uint32_t num_stats, const char **names, int32_t *values) {
|
|
if (s_stats_num_columns == 0) {
|
|
// Save the column names if this is the first row
|
|
s_stats_num_columns = num_stats;
|
|
for (int i = 0; i < num_stats; i++) {
|
|
s_stats_column_names[i] = strdup(names[i]);
|
|
}
|
|
}
|
|
|
|
cl_assert_equal_i(num_stats, s_stats_num_columns);
|
|
|
|
// Create a new row of stats
|
|
StatsRow *stats = malloc(sizeof(StatsRow));
|
|
memset(stats, 0, sizeof(*stats));
|
|
|
|
// Collect the stats and also print them out
|
|
for (int i = 0; i < num_stats; i++) {
|
|
printf("%s: %d, ", names[i], values[i]);
|
|
stats->values[i] = values[i];
|
|
}
|
|
printf("\n");
|
|
|
|
// Append to the list
|
|
if (s_stat_rows) {
|
|
list_append(&s_stat_rows->node, &stats->node);
|
|
} else {
|
|
s_stat_rows = stats;
|
|
}
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Set a specific column in the last row by name
|
|
static void prv_stats_set_last_row_value(const char* name, uint32_t value) {
|
|
if (s_stat_rows == NULL) {
|
|
return;
|
|
}
|
|
StatsRow *stats = (StatsRow *)list_get_tail(&s_stat_rows->node);
|
|
cl_assert(stats != NULL);
|
|
bool found = 0;
|
|
for (int i = 0; i < s_stats_num_columns; i++) {
|
|
if (strcmp(s_stats_column_names[i], name) == 0) {
|
|
found = true;
|
|
stats->values[i] = value;
|
|
break;
|
|
}
|
|
}
|
|
cl_assert(found);
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Write out accumulated stats to a csv file
|
|
static void prv_stats_write(const char* filename, bool create, const char *test_name,
|
|
bool is_stepping) {
|
|
if (!s_stat_rows) {
|
|
return;
|
|
}
|
|
|
|
FILE *file = NULL;
|
|
if (create) {
|
|
// Write the column names when creating the file
|
|
file = fopen(filename, "w");
|
|
cl_assert(file);
|
|
|
|
fprintf(file, "test, epoch_type, epoch_idx");
|
|
for (int i = 0; i < s_stats_num_columns; i++) {
|
|
fprintf(file, ", %s", s_stats_column_names[i]);
|
|
}
|
|
fprintf(file, "\n");
|
|
} else {
|
|
file = fopen(filename, "a");
|
|
cl_assert(file);
|
|
}
|
|
|
|
// Write out the column values for each row
|
|
StatsRow *row = s_stat_rows;
|
|
int row_idx = 0;
|
|
for (; row != NULL; row = (StatsRow *)row->node.next, row_idx++) {
|
|
fprintf(file, "\"%s\"", test_name);
|
|
StatsEpochType epoch_type;
|
|
if (!is_stepping) {
|
|
epoch_type = StatsEpochTypeNonStepping;
|
|
} else if (row_idx == 0 || !row->node.next) {
|
|
// We consider the first and last epoch of each sample as a "partial stepping" epoch.
|
|
epoch_type = StatsEpochTypePartialStepping;
|
|
} else {
|
|
epoch_type = StatsEpochTypeStepping;
|
|
}
|
|
fprintf(file, " ,%d, %d", (int)epoch_type, row_idx);
|
|
for (int i = 0; i < s_stats_num_columns; i++) {
|
|
fprintf(file, " ,%d", row->values[i]);
|
|
}
|
|
fprintf(file, "\n");
|
|
}
|
|
|
|
cl_assert_equal_i(0, fclose(file));
|
|
printf("Stats written to file: %s", filename);
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Run samples through the algorithm integrated into the firmware
|
|
// @param[in] data array of samples
|
|
// @param[in] num_samples size of data array
|
|
// @param[in] minute_data array to return minute data in
|
|
// @param[in,out] minute_data_len size of minute_data array on entry, # of filled entries
|
|
// on exit
|
|
// @param return number of steps computed.
|
|
static uint32_t prv_feed_kalg_samples(AccelRawData *data, int num_samples,
|
|
TestMinuteData *minute_data_array, int *minute_data_len) {
|
|
uint32_t total_steps = 0;
|
|
uint32_t minute_steps = 0;
|
|
int num_minutes_captured = 0;
|
|
int num_samples_left = num_samples;
|
|
|
|
// Init state
|
|
s_kalg_state = kernel_zalloc(kalg_state_size());
|
|
kalg_init(s_kalg_state, prv_stats_cb);
|
|
|
|
// Run some data through it, 1 minute at a time
|
|
while (num_samples_left) {
|
|
int chunk_size = MIN(num_samples_left, KALG_SAMPLE_HZ * SECONDS_PER_MINUTE);
|
|
uint32_t steps;
|
|
uint32_t consumed_samples;
|
|
steps = kalg_analyze_samples(s_kalg_state, data, chunk_size, &consumed_samples);
|
|
minute_steps += steps;
|
|
total_steps += steps;
|
|
|
|
if (chunk_size == KALG_SAMPLE_HZ * SECONDS_PER_MINUTE) {
|
|
// Capture the minute data for each minute
|
|
TestMinuteData minute_data = {
|
|
.steps = minute_steps,
|
|
};
|
|
bool still;
|
|
kalg_minute_stats(s_kalg_state, &minute_data.vmc, &minute_data.orientation, &still);
|
|
|
|
PBL_ASSERTN(num_minutes_captured < *minute_data_len);
|
|
minute_data_array[num_minutes_captured++] = minute_data;
|
|
minute_steps = 0;
|
|
}
|
|
num_samples_left -= chunk_size;
|
|
data += chunk_size;
|
|
}
|
|
|
|
// -------------------------------------------------------
|
|
// Leftover data in epoch, if any
|
|
total_steps += kalg_analyze_finish_epoch(s_kalg_state);
|
|
|
|
TestMinuteData minute_data = {
|
|
.steps = minute_steps,
|
|
};
|
|
bool still;
|
|
kalg_minute_stats(s_kalg_state, &minute_data.vmc, &minute_data.orientation, &still);
|
|
PBL_ASSERTN(num_minutes_captured < *minute_data_len);
|
|
minute_data_array[num_minutes_captured++] = minute_data;
|
|
|
|
// ----------------------------------------------------
|
|
// Free state
|
|
kernel_free(s_kalg_state);
|
|
s_kalg_state = NULL;
|
|
|
|
*minute_data_len = num_minutes_captured;
|
|
return total_steps;
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Run samples through the reference algorithm.
|
|
static uint32_t prv_feed_reference_samples(AccelRawData *data, int num_samples) {
|
|
extern int ref_accel_data_handler(AccelData *data, uint32_t num_samples );
|
|
extern void ref_init(void);
|
|
extern int ref_finish_epoch(void);
|
|
extern void ref_minute_stats(uint8_t *orientation, uint8_t *vmc);
|
|
|
|
int steps = 0;
|
|
uint8_t orientation, vmc;
|
|
|
|
ref_init();
|
|
AccelData accel_buf[KALG_SAMPLE_HZ];
|
|
|
|
int chunk_size = 0;
|
|
int samples_in_minute = 0;
|
|
for (uint32_t i = 0; i < num_samples; i++) {
|
|
accel_buf[chunk_size++] = (AccelData) {
|
|
.x = data[i].x,
|
|
.y = data[i].y,
|
|
.z = data[i].z
|
|
};
|
|
samples_in_minute++;
|
|
if (chunk_size == KALG_SAMPLE_HZ) {
|
|
steps = ref_accel_data_handler(accel_buf, chunk_size);
|
|
chunk_size = 0;
|
|
}
|
|
if (samples_in_minute >= KALG_SAMPLE_HZ * SECONDS_PER_MINUTE) {
|
|
ref_minute_stats(&orientation, &vmc);
|
|
samples_in_minute = 0;
|
|
}
|
|
}
|
|
|
|
// leftover data, if any
|
|
if (chunk_size > 0) {
|
|
steps = ref_accel_data_handler(accel_buf, chunk_size);
|
|
}
|
|
steps = ref_finish_epoch();
|
|
ref_minute_stats(&orientation, &vmc);
|
|
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "processed %d samples (%d seconds) of data: %d steps",
|
|
num_samples, num_samples / KALG_SAMPLE_HZ, steps);
|
|
return steps;
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------------
|
|
// The file discovery state definitions
|
|
typedef enum {
|
|
SampleFileType_AccelSamples,
|
|
SampleFileType_MinuteSamples,
|
|
} SampleFileType;
|
|
|
|
typedef struct {
|
|
char *res_path; // path to directory containing sample files
|
|
DIR *dp; // Directory pointer
|
|
struct dirent *ep; // Entry we are currently processing
|
|
FILE *file; // File we currently have open
|
|
SampleFileType type; // type of samples
|
|
} SampleDiscoveryState;
|
|
|
|
#define ACCEL_SAMPLES_DISCOVERY_MAX_SAMPLES (12 * SECONDS_PER_MINUTE * KALG_SAMPLE_HZ)
|
|
typedef struct {
|
|
SampleDiscoveryState common;
|
|
AccelRawData samples[ACCEL_SAMPLES_DISCOVERY_MAX_SAMPLES];
|
|
StepFileTestEntry test_entry;
|
|
} AccelSampleDiscoveryState;
|
|
static AccelSampleDiscoveryState s_accel_sample_discovery_state;
|
|
|
|
#define SLEEP_SAMPLES_DISCOVERY_MAX_SAMPLES (40 * MINUTES_PER_HOUR)
|
|
typedef struct {
|
|
SampleDiscoveryState common;
|
|
AlgMinuteFileSample samples[SLEEP_SAMPLES_DISCOVERY_MAX_SAMPLES];
|
|
SleepFileTestEntry test_entry;
|
|
} SleepSampleDiscoveryState;
|
|
static SleepSampleDiscoveryState s_sleep_sample_discovery_state;
|
|
|
|
#define ACTIVITY_SAMPLES_DISCOVERY_MAX_SAMPLES (40 * MINUTES_PER_HOUR)
|
|
typedef struct {
|
|
SampleDiscoveryState common;
|
|
AlgMinuteFileSample samples[ACTIVITY_SAMPLES_DISCOVERY_MAX_SAMPLES];
|
|
ActivityFileTestEntry test_entry;
|
|
} ActivitySampleDiscoveryState;
|
|
static ActivitySampleDiscoveryState s_activity_sample_discovery_state;
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
static bool prv_parse_accel_samples_file(AccelSampleDiscoveryState *state) {
|
|
// Init for next set of samples
|
|
state->test_entry = (StepFileTestEntry) {
|
|
.samples = state->samples,
|
|
.exp_steps = -1,
|
|
.exp_steps_min = -1,
|
|
.exp_steps_max = -1,
|
|
.weight = 1.0,
|
|
};
|
|
|
|
char line_buf[256];
|
|
while (true) {
|
|
char *line = fgets(line_buf, sizeof(line_buf), state->common.file);
|
|
if (!line) {
|
|
// EOF
|
|
break;
|
|
}
|
|
//printf("\nGot line: %s", line);
|
|
|
|
// Find first token
|
|
char *token = strtok(line, " \t\n");
|
|
if (!token) {
|
|
continue;
|
|
}
|
|
|
|
// If this is a pre-processor directive, skip it
|
|
if (token[0] == '#') {
|
|
continue;
|
|
}
|
|
|
|
// If this is a comment skip it
|
|
if (strcmp(token, "//") == 0) {
|
|
continue;
|
|
}
|
|
|
|
// If this is an AccelRawData line, get the name
|
|
if (strcmp(token, "AccelRawData") == 0) {
|
|
PBL_ASSERT(state->test_entry.name[0] == 0, "Unexpected start of new samples");
|
|
|
|
token = strtok(NULL, "(");
|
|
// Copy starting from token + 1 to skip the '*' at the front
|
|
strncpy(state->test_entry.name, token + 1, sizeof(state->test_entry.name));
|
|
printf("\nParsing function samples: %s", state->test_entry.name);
|
|
continue;
|
|
}
|
|
|
|
// Look for and parse the expected values
|
|
if (strcmp(token, "//>") == 0) {
|
|
token = strtok(NULL, " \t\n");
|
|
if (strcmp(token, "TEST_EXPECTED") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.exp_steps);
|
|
} else if (strcmp(token, "TEST_EXPECTED_MIN") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.exp_steps_min);
|
|
} else if (strcmp(token, "TEST_EXPECTED_MAX") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.exp_steps_max);
|
|
} else if (strcmp(token, "TEST_WEIGHT") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%f", &state->test_entry.weight);
|
|
} else if (strcmp(token, "TEST_NAME") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%s", state->test_entry.name);
|
|
}
|
|
}
|
|
|
|
// If this is a "static AccelRawData samples[] = {" line, skip it
|
|
if (strcmp(token, "static") == 0) {
|
|
continue;
|
|
}
|
|
|
|
// Grab a sample
|
|
if (strcmp(token, "{") == 0) {
|
|
PBL_ASSERTN(state->test_entry.name[0] != 0);
|
|
int x, y, z;
|
|
sscanf(token + strlen(token) + 1, "%d, %d, %d", &x, &y, &z);
|
|
fflush(stdout);
|
|
|
|
PBL_ASSERTN(state->test_entry.num_samples < ACCEL_SAMPLES_DISCOVERY_MAX_SAMPLES);
|
|
state->samples[state->test_entry.num_samples++] = (AccelRawData) {
|
|
.x = x,
|
|
.y = y,
|
|
.z = z,
|
|
};
|
|
continue;
|
|
}
|
|
|
|
// End of a sample
|
|
if (strcmp(token, "}") == 0) {
|
|
PBL_ASSERTN(state->test_entry.name[0] != 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Did we get samples?
|
|
if (state->test_entry.num_samples > 0) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
static bool prv_parse_sleep_samples_file(SleepSampleDiscoveryState *state) {
|
|
// Init for next set of samples
|
|
state->test_entry = (SleepFileTestEntry) {
|
|
.samples = state->samples,
|
|
.version = 1,
|
|
.total = {-1, -1, -1},
|
|
.deep = {-1, -1, -1},
|
|
.start_at = {-1, -1, -1},
|
|
.end_at = {-1, -1, -1},
|
|
.cur_state_elapsed = {-1, -1, -1},
|
|
.in_sleep = {-1, -1, -1},
|
|
.in_deep_sleep = {-1, -1, -1},
|
|
.weight = 1.0,
|
|
.force_shut_down_at = -1,
|
|
};
|
|
|
|
char line_buf[256];
|
|
while (true) {
|
|
char *line = fgets(line_buf, sizeof(line_buf), state->common.file);
|
|
if (!line) {
|
|
// EOF
|
|
break;
|
|
}
|
|
//printf("\nGot line: %s", line);
|
|
|
|
// Find first token
|
|
char *token = strtok(line, " \t\n");
|
|
if (!token) {
|
|
continue;
|
|
}
|
|
|
|
// If this is a pre-processor directive, skip it
|
|
if (token[0] == '#') {
|
|
continue;
|
|
}
|
|
|
|
// If this is a comment skip it
|
|
if (strcmp(token, "//") == 0) {
|
|
continue;
|
|
}
|
|
|
|
// If this is an AlgDlsMinuteData line, get the name
|
|
if (strcmp(token, "AlgDlsMinuteData") == 0) {
|
|
PBL_ASSERT(state->test_entry.name[0] == 0, "Unexpected start of new samples");
|
|
|
|
token = strtok(NULL, "(");
|
|
// Copy starting from token + 1 to skip the '*' at the front
|
|
strncpy(state->test_entry.name, token + 1, sizeof(state->test_entry.name));
|
|
printf("\nParsing function samples: %s", state->test_entry.name);
|
|
continue;
|
|
}
|
|
|
|
// Look for and parse the expected values
|
|
if (strcmp(token, "//>") == 0) {
|
|
token = strtok(NULL, " \t\n");
|
|
if (strcmp(token, "TEST_VERSION") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.version);
|
|
|
|
} else if (strcmp(token, "TEST_TOTAL") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.total.value);
|
|
} else if (strcmp(token, "TEST_TOTAL_MIN") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.total.min);
|
|
} else if (strcmp(token, "TEST_TOTAL_MAX") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.total.max);
|
|
|
|
} else if (strcmp(token, "TEST_DEEP") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.deep.value);
|
|
} else if (strcmp(token, "TEST_DEEP_MIN") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.deep.min);
|
|
} else if (strcmp(token, "TEST_DEEP_MAX") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.deep.max);
|
|
|
|
} else if (strcmp(token, "TEST_START_AT") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.start_at.value);
|
|
} else if (strcmp(token, "TEST_START_AT_MIN") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.start_at.min);
|
|
} else if (strcmp(token, "TEST_START_AT_MAX") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.start_at.max);
|
|
|
|
} else if (strcmp(token, "TEST_END_AT") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.end_at.value);
|
|
} else if (strcmp(token, "TEST_END_AT_MIN") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.end_at.min);
|
|
} else if (strcmp(token, "TEST_END_AT_MAX") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.end_at.max);
|
|
|
|
|
|
} else if (strcmp(token, "TEST_CUR_STATE_ELAPSED") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.cur_state_elapsed.value);
|
|
} else if (strcmp(token, "TEST_CUR_STATE_ELAPSED_MIN") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.cur_state_elapsed.min);
|
|
} else if (strcmp(token, "TEST_CUR_STATE_ELAPSED_MAX") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.cur_state_elapsed.max);
|
|
|
|
|
|
} else if (strcmp(token, "TEST_IN_SLEEP") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.in_sleep.value);
|
|
} else if (strcmp(token, "TEST_IN_SLEEP_MIN") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.in_sleep.min);
|
|
} else if (strcmp(token, "TEST_IN_SLEEP_MAX") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.in_sleep.max);
|
|
|
|
|
|
} else if (strcmp(token, "TEST_IN_DEEP_SLEEP") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.in_deep_sleep.value);
|
|
} else if (strcmp(token, "TEST_IN_DEEP_SLEEP_MIN") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.in_deep_sleep.min);
|
|
} else if (strcmp(token, "TEST_IN_DEEP_SLEEP_MAX") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.in_deep_sleep.max);
|
|
|
|
} else if (strcmp(token, "TEST_FORCE_SHUT_DOWN_AT") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.force_shut_down_at);
|
|
|
|
} else if (strcmp(token, "TEST_WEIGHT") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%f", &state->test_entry.weight);
|
|
} else if (strcmp(token, "TEST_NAME") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%s", state->test_entry.name);
|
|
}
|
|
}
|
|
|
|
// If this is a "static AlgDlsMinuteData samples[] = {" line, skip it
|
|
if (strcmp(token, "static") == 0) {
|
|
continue;
|
|
}
|
|
|
|
// Grab a sample
|
|
if (strcmp(token, "{") == 0) {
|
|
PBL_ASSERTN(state->test_entry.name[0] != 0);
|
|
int steps = 0;
|
|
int orientation = 0;
|
|
int vmc = 0;
|
|
int light = 0;
|
|
int plugged_in = 0;
|
|
if (state->test_entry.version == 1) {
|
|
sscanf(token + strlen(token) + 1, "%d, 0x%x, %d", &steps, &orientation, &vmc);
|
|
} else if (state->test_entry.version == 2) {
|
|
sscanf(token + strlen(token) + 1, "%d, 0x%x, %d, %d", &steps, &orientation, &vmc, &light);
|
|
} else if (state->test_entry.version == 3) {
|
|
sscanf(token + strlen(token) + 1, "%d, 0x%x, %d, %d, %d", &steps, &orientation, &vmc,
|
|
&light, &plugged_in);
|
|
} else {
|
|
cl_assert(false);
|
|
}
|
|
fflush(stdout);
|
|
|
|
PBL_ASSERTN(state->test_entry.num_samples < SLEEP_SAMPLES_DISCOVERY_MAX_SAMPLES);
|
|
state->samples[state->test_entry.num_samples++] = (AlgMinuteFileSample) {
|
|
.v5_fields = {
|
|
.steps = steps,
|
|
.orientation = orientation,
|
|
.vmc = vmc,
|
|
.light = light,
|
|
.plugged_in = plugged_in,
|
|
},
|
|
};
|
|
continue;
|
|
}
|
|
|
|
// End of a sample
|
|
if (strcmp(token, "}") == 0) {
|
|
PBL_ASSERTN(state->test_entry.name[0] != 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Did we get samples?
|
|
if (state->test_entry.num_samples > 0) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
static bool prv_parse_activity_samples_file(ActivitySampleDiscoveryState *state) {
|
|
// Init for next set of samples
|
|
state->test_entry = (ActivityFileTestEntry) {
|
|
.samples = state->samples,
|
|
.version = 1,
|
|
.activity_type = {-1, -1, -1},
|
|
.len = {-1, -1, -1},
|
|
.start_at = {-1, -1, -1},
|
|
.weight = 1.0,
|
|
.force_shut_down_at = -1,
|
|
};
|
|
|
|
char line_buf[256];
|
|
while (true) {
|
|
char *line = fgets(line_buf, sizeof(line_buf), state->common.file);
|
|
if (!line) {
|
|
// EOF
|
|
break;
|
|
}
|
|
// printf("\nGot line: %s", line);
|
|
|
|
// Find first token
|
|
char *token = strtok(line, " \t\n");
|
|
if (!token) {
|
|
continue;
|
|
}
|
|
|
|
// If this is a pre-processor directive, skip it
|
|
if (token[0] == '#') {
|
|
continue;
|
|
}
|
|
|
|
// If this is a comment skip it
|
|
if (strcmp(token, "//") == 0) {
|
|
continue;
|
|
}
|
|
|
|
// If this is an AlgDlsMinuteData line, get the name
|
|
if (strcmp(token, "AlgDlsMinuteData") == 0) {
|
|
PBL_ASSERT(state->test_entry.name[0] == 0, "Unexpected start of new samples");
|
|
|
|
token = strtok(NULL, "(");
|
|
// Copy starting from token + 1 to skip the '*' at the front
|
|
strncpy(state->test_entry.name, token + 1, sizeof(state->test_entry.name));
|
|
printf("\nParsing function samples: %s", state->test_entry.name);
|
|
continue;
|
|
}
|
|
|
|
// Look for and parse the expected values
|
|
if (strcmp(token, "//>") == 0) {
|
|
token = strtok(NULL, " \t\n");
|
|
if (strcmp(token, "TEST_VERSION") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.version);
|
|
|
|
} else if (strcmp(token, "TEST_ACTIVITY_TYPE") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.activity_type.value);
|
|
} else if (strcmp(token, "TEST_ACTIVITY_TYPE_MIN") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.activity_type.min);
|
|
} else if (strcmp(token, "TEST_ACTIVITY_TYPE_MAX") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.activity_type.max);
|
|
|
|
} else if (strcmp(token, "TEST_LEN") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.len.value);
|
|
} else if (strcmp(token, "TEST_LEN_MIN") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.len.min);
|
|
} else if (strcmp(token, "TEST_LEN_MAX") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.len.max);
|
|
|
|
} else if (strcmp(token, "TEST_START_AT") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.start_at.value);
|
|
} else if (strcmp(token, "TEST_START_AT_MIN") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.start_at.min);
|
|
} else if (strcmp(token, "TEST_START_AT_MAX") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.start_at.max);
|
|
} else if (strcmp(token, "TEST_FORCE_SHUT_DOWN_AT") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%d", &state->test_entry.force_shut_down_at);
|
|
|
|
} else if (strcmp(token, "TEST_WEIGHT") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%f", &state->test_entry.weight);
|
|
} else if (strcmp(token, "TEST_NAME") == 0) {
|
|
sscanf(token + strlen(token) + 1, "%s", state->test_entry.name);
|
|
}
|
|
}
|
|
|
|
// If this is a "static AlgDlsMinuteData samples[] = {" line, skip it
|
|
if (strcmp(token, "static") == 0) {
|
|
continue;
|
|
}
|
|
|
|
// Grab a sample
|
|
if (strcmp(token, "{") == 0) {
|
|
PBL_ASSERTN(state->test_entry.name[0] != 0);
|
|
int steps = 0;
|
|
int orientation = 0;
|
|
int vmc = 0;
|
|
int light = 0;
|
|
int plugged_in = 0;
|
|
if (state->test_entry.version == 1) {
|
|
sscanf(token + strlen(token) + 1, "%d, 0x%x, %d", &steps, &orientation, &vmc);
|
|
} else if (state->test_entry.version == 2) {
|
|
sscanf(token + strlen(token) + 1, "%d, 0x%x, %d, %d", &steps, &orientation, &vmc, &light);
|
|
} else if (state->test_entry.version == 3) {
|
|
sscanf(token + strlen(token) + 1, "%d, 0x%x, %d, %d, %d", &steps, &orientation, &vmc,
|
|
&light, &plugged_in);
|
|
} else {
|
|
cl_assert(false);
|
|
}
|
|
fflush(stdout);
|
|
|
|
PBL_ASSERTN(state->test_entry.num_samples < SLEEP_SAMPLES_DISCOVERY_MAX_SAMPLES);
|
|
state->samples[state->test_entry.num_samples++] = (AlgMinuteFileSample) {
|
|
.v5_fields = {
|
|
.steps = steps,
|
|
.orientation = orientation,
|
|
.vmc = vmc,
|
|
.light = light,
|
|
.plugged_in = plugged_in,
|
|
},
|
|
};
|
|
continue;
|
|
}
|
|
|
|
// End of a sample
|
|
if (strcmp(token, "}") == 0) {
|
|
PBL_ASSERTN(state->test_entry.name[0] != 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Did we get samples?
|
|
if (state->test_entry.num_samples > 0) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Init the sample discovery iterator
|
|
static bool prv_sample_discovery_init(SampleDiscoveryState *state, SampleFileType samples_type,
|
|
const char *test_files_path) {
|
|
|
|
// Free prior one
|
|
if (state->dp) {
|
|
if (state->file) {
|
|
fclose(state->file);
|
|
state->file = NULL;
|
|
}
|
|
closedir(state->dp);
|
|
state->dp = NULL;
|
|
free(state->res_path);
|
|
}
|
|
|
|
// Open up the directory
|
|
state->res_path = malloc(strlen(CLAR_FIXTURE_PATH) + strlen(test_files_path) + 2);
|
|
sprintf(state->res_path, "%s/%s", CLAR_FIXTURE_PATH, test_files_path);
|
|
state->dp = opendir(state->res_path);
|
|
|
|
if (state->dp == NULL) {
|
|
printf("\nCould not open directory %s", state->res_path);
|
|
return false;
|
|
}
|
|
|
|
state->type = samples_type;
|
|
return true;
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Advance to the next file in the directory. Return true if successful
|
|
static bool prv_sample_discovery_next_file(SampleDiscoveryState *state) {
|
|
if (state->dp == NULL) {
|
|
return false;
|
|
}
|
|
|
|
while (!state->file) {
|
|
state->ep = readdir(state->dp);
|
|
if (!state->ep) {
|
|
// No more files
|
|
return false;
|
|
}
|
|
|
|
// See if it's the right extension
|
|
int name_len = strlen(state->ep->d_name);
|
|
if (name_len < 3 || (strcmp(state->ep->d_name + name_len - 2, ".c") != 0)) {
|
|
continue;
|
|
}
|
|
|
|
// Open up the file
|
|
printf("\n\n\n\nParsing file: %s", state->ep->d_name);
|
|
char file_path[strlen(state->res_path) + strlen(state->ep->d_name) + 1];
|
|
sprintf(file_path, "%s/%s", state->res_path, state->ep->d_name);
|
|
|
|
state->file = fopen(file_path, "r");
|
|
if (!state->file) {
|
|
printf("\nFile %s could not be opened", file_path);
|
|
continue;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Return info on the next set of samples
|
|
static bool prv_accel_sample_discovery_next(StepFileTestEntry *entry) {
|
|
AccelSampleDiscoveryState *state = &s_accel_sample_discovery_state;
|
|
|
|
while (true) {
|
|
// Read next entry if necessary
|
|
if (!state->common.file) {
|
|
if (!prv_sample_discovery_next_file(&state->common)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Parse next set of samples in the current file
|
|
bool success = prv_parse_accel_samples_file(state);
|
|
if (success) {
|
|
*entry = state->test_entry;
|
|
return true;
|
|
} else {
|
|
// No more in this file
|
|
fclose(state->common.file);
|
|
state->common.file = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Return info on the next set of samples
|
|
static bool prv_sleep_sample_discovery_next(SleepFileTestEntry *entry) {
|
|
SleepSampleDiscoveryState *state = &s_sleep_sample_discovery_state;
|
|
|
|
while (true) {
|
|
// Read next entry if necessary
|
|
if (!state->common.file) {
|
|
if (!prv_sample_discovery_next_file(&state->common)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Parse next set of samples in the current file
|
|
bool success = prv_parse_sleep_samples_file(state);
|
|
if (success) {
|
|
*entry = state->test_entry;
|
|
return true;
|
|
} else {
|
|
// No more in this file
|
|
fclose(state->common.file);
|
|
state->common.file = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Return info on the next set of samples
|
|
static bool prv_activity_sample_discovery_next(ActivityFileTestEntry *entry) {
|
|
ActivitySampleDiscoveryState *state = &s_activity_sample_discovery_state;
|
|
|
|
while (true) {
|
|
// Read next entry if necessary
|
|
if (!state->common.file) {
|
|
if (!prv_sample_discovery_next_file(&state->common)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Parse next set of samples in the current file
|
|
bool success = prv_parse_activity_samples_file(state);
|
|
if (success) {
|
|
*entry = state->test_entry;
|
|
return true;
|
|
} else {
|
|
// No more in this file
|
|
fclose(state->common.file);
|
|
state->common.file = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// Callback provided to the qsort() routine for sorting step tests by name
|
|
int prv_qsort_step_test_entry_cb(const void *a, const void *b) {
|
|
StepFileTestEntry *entry_a = (StepFileTestEntry *)a;
|
|
StepFileTestEntry *entry_b = (StepFileTestEntry *)b;
|
|
|
|
// Put the non-walking samples at the end
|
|
if (entry_a->exp_steps > 0 && entry_b->exp_steps == 0) {
|
|
return -1;
|
|
} else if (entry_a->exp_steps == 0 && entry_b->exp_steps > 0) {
|
|
return +1;
|
|
} else {
|
|
return strcmp(entry_a->name, entry_b->name);
|
|
}
|
|
}
|
|
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// Callback provided to the qsort() routine for sorting sleep tests by name
|
|
int prv_qsort_sleep_test_entry_cb(const void *a, const void *b) {
|
|
SleepFileTestEntry *entry_a = (SleepFileTestEntry *)a;
|
|
SleepFileTestEntry *entry_b = (SleepFileTestEntry *)b;
|
|
|
|
return strcmp(entry_a->name, entry_b->name);
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// Callback provided to the qsort() routine for sorting activity tests by name
|
|
int prv_qsort_activity_test_entry_cb(const void *a, const void *b) {
|
|
ActivityFileTestEntry *entry_a = (ActivityFileTestEntry *)a;
|
|
ActivityFileTestEntry *entry_b = (ActivityFileTestEntry *)b;
|
|
|
|
return strcmp(entry_a->name, entry_b->name);
|
|
}
|
|
|
|
|
|
|
|
// =============================================================================================
|
|
// Support for capturing activity sessions detected by the algorithm
|
|
typedef struct {
|
|
uint16_t steps;
|
|
uint32_t resting_calories;
|
|
uint32_t active_calories;
|
|
uint32_t distance_mm;
|
|
} KAlgTestActivityMinute;
|
|
|
|
#define MAX_CAPTURED_SESSIONS 32
|
|
KAlgTestActivitySession s_captured_activity_sessions[MAX_CAPTURED_SESSIONS];
|
|
int s_num_captured_activity_sessions;
|
|
void prv_activity_session_callback(void *context, KAlgActivityType activity_type,
|
|
time_t start_utc, uint32_t len_sec, bool ongoing, bool delete,
|
|
uint32_t steps, uint32_t resting_calories,
|
|
uint32_t active_calories, uint32_t distance_mm) {
|
|
int entry_idx = s_num_captured_activity_sessions;
|
|
// Ignore sleep activities for this test
|
|
if ((activity_type == KAlgActivityType_Sleep)
|
|
|| (activity_type == KAlgActivityType_RestfulSleep)) {
|
|
return;
|
|
}
|
|
|
|
// If this activity already exists, update it
|
|
KAlgTestActivitySession *session = s_captured_activity_sessions;
|
|
for (int i = 0; i < s_num_captured_activity_sessions; i++, session++) {
|
|
if (session->start_utc == start_utc && session->activity == activity_type) {
|
|
entry_idx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (delete && (s_num_captured_activity_sessions > 0)) {
|
|
if (entry_idx == s_num_captured_activity_sessions) {
|
|
return;
|
|
}
|
|
int num_to_move = s_num_captured_activity_sessions - entry_idx - 1;
|
|
memmove(&s_captured_activity_sessions[entry_idx], &s_captured_activity_sessions[entry_idx + 1],
|
|
num_to_move * sizeof(KAlgTestActivitySession));
|
|
s_num_captured_activity_sessions--;
|
|
}
|
|
|
|
cl_assert(entry_idx < MAX_CAPTURED_SESSIONS);
|
|
s_captured_activity_sessions[entry_idx] = (KAlgTestActivitySession) {
|
|
.activity = activity_type,
|
|
.len_minutes = len_sec / SECONDS_PER_MINUTE,
|
|
.start_utc = start_utc,
|
|
.ongoing = ongoing,
|
|
.steps = steps,
|
|
.active_calories = active_calories,
|
|
.resting_calories = resting_calories,
|
|
.distance_mm = distance_mm,
|
|
};
|
|
|
|
printf("\nAdded new activity: %d, start_utc: %d, len_m: %d", (int)activity_type,
|
|
(int)start_utc, (int)len_sec / SECONDS_PER_MINUTE);
|
|
if (entry_idx == s_num_captured_activity_sessions) {
|
|
s_num_captured_activity_sessions++;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ----------------------------------------------------------------------------------------
|
|
// Print a timestamp in a format useful for log messages (for debugging). This only prints
|
|
// the hour and minute: HH:MM
|
|
static const char* prv_log_time(time_t utc) {
|
|
static char time_str[8];
|
|
int minutes = (utc / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR;
|
|
int hours = (utc / SECONDS_PER_HOUR) % HOURS_PER_DAY;
|
|
|
|
snprintf(time_str, sizeof(time_str), "%02d:%02d", hours, minutes);
|
|
return time_str;
|
|
}
|
|
|
|
|
|
// =============================================================================================
|
|
// Start of unit tests
|
|
void test_kraepelin_algorithm__initialize(void) {
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
void test_kraepelin_algorithm__cleanup(void) {
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
void test_kraepelin_algorithm__step_tests(void) {
|
|
bool success = prv_sample_discovery_init(&s_accel_sample_discovery_state.common,
|
|
SampleFileType_AccelSamples, "activity/step_samples");
|
|
cl_assert(success);
|
|
|
|
const uint32_t k_max_tests = 1000;
|
|
uint32_t num_tests = 0;
|
|
|
|
// Results
|
|
typedef struct {
|
|
int steps;
|
|
int ref_steps;
|
|
uint32_t test_idx;
|
|
} StepTestResults;
|
|
StepTestResults test_results[k_max_tests];
|
|
StepFileTestEntry test_entry[k_max_tests];
|
|
|
|
while (prv_accel_sample_discovery_next(&test_entry[num_tests])) {
|
|
StepFileTestEntry *entry = &test_entry[num_tests];
|
|
|
|
#ifdef STEP_TEST_ONLY
|
|
if (strcmp(entry->name, STEP_TEST_ONLY)) {
|
|
continue;
|
|
}
|
|
#endif
|
|
entry->test_idx = num_tests;
|
|
|
|
printf("\n\n========================================================");
|
|
printf("\nRunning sample set: \"%s\"\n", entry->name);
|
|
|
|
// Run the step algorithm
|
|
int minute_data_len = 100;
|
|
TestMinuteData minute_data[minute_data_len];
|
|
prv_stats_reinit();
|
|
int steps = prv_feed_kalg_samples(entry->samples, entry->num_samples, minute_data,
|
|
&minute_data_len);
|
|
// Save stats to file
|
|
#ifdef STATS_FILE_NAME
|
|
prv_stats_write(STATS_FILE_NAME, (num_tests == 0) /*create*/, entry->name,
|
|
(entry->exp_steps != 0) /*stepping*/);
|
|
#endif
|
|
|
|
|
|
// Run through reference code
|
|
int ref_steps = prv_feed_reference_samples(entry->samples, entry->num_samples);
|
|
//int ref_steps = -1;
|
|
|
|
int error = abs(steps - entry->exp_steps);
|
|
float weighted_error = (float)error * entry->weight;
|
|
printf("\nRESULTS: exp_steps: %d, act_steps: %d, ref_steps: %d, error: %d, weighted_error: %f",
|
|
entry->exp_steps, (int)steps, (int)ref_steps, (int)error, weighted_error);
|
|
printf("\n min: (steps, vmc, orientation)");
|
|
for (int j = 0; j < minute_data_len; j++) {
|
|
printf("\n %-4d %-4d 0x%-4x", (int)minute_data[j].steps,
|
|
(int)minute_data[j].vmc, (int)minute_data[j].orientation);
|
|
}
|
|
test_results[num_tests] = (StepTestResults) {
|
|
.steps = steps,
|
|
.ref_steps = ref_steps,
|
|
.test_idx = num_tests,
|
|
};
|
|
|
|
num_tests++;
|
|
if (num_tests >= k_max_tests) {
|
|
printf("RAN INTO MAX NUMBER OF TESTS WE SUPPORT");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Make sure we discovered at least 1 test
|
|
cl_assert(num_tests > 0);
|
|
|
|
// Let's sort the tests by name
|
|
qsort(&test_entry[0], num_tests, sizeof(test_entry[0]), prv_qsort_step_test_entry_cb);
|
|
|
|
// ------------------------------------------------------------------------------------------
|
|
// Print summery of results
|
|
printf("\n\n");
|
|
printf("\n%-40s %-10s %-10s %-10s %-10s %-10s %-10s %-10s %-10s",
|
|
"name",
|
|
"exp_steps",
|
|
"act_steps",
|
|
"error",
|
|
"min",
|
|
"max",
|
|
"ref_steps",
|
|
"weight_err",
|
|
"status");
|
|
printf("\n---------------------------------------------------------------------------------"
|
|
"-----------------------------");
|
|
|
|
float weighted_sum = 0.0;
|
|
int pass_count = 0;
|
|
int fail_count = 0;
|
|
StepFileTestEntry *entry = &test_entry[0];
|
|
StepTestResults *results;
|
|
for (int i = 0; i < num_tests; i++, entry++) {
|
|
results = &test_results[entry->test_idx];
|
|
cl_assert_equal_i(results->test_idx, entry->test_idx);
|
|
int error = results->steps - entry->exp_steps;
|
|
float weighted_error = (float)abs(error) * entry->weight;
|
|
weighted_sum += weighted_error;
|
|
char *status;
|
|
if (results->steps < entry->exp_steps_min || results->steps > entry->exp_steps_max) {
|
|
status = "FAIL";
|
|
fail_count++;
|
|
} else {
|
|
status = "pass";
|
|
pass_count++;
|
|
}
|
|
printf("\n%-40s %-10d %-10d %-10d %-10d %-10d %-10d %-10.2f %-10s",
|
|
entry->name,
|
|
entry->exp_steps,
|
|
results->steps,
|
|
error,
|
|
entry->exp_steps_min,
|
|
entry->exp_steps_max,
|
|
results->ref_steps,
|
|
weighted_error,
|
|
status);
|
|
}
|
|
|
|
if (fail_count) {
|
|
printf("\n\ntest FAILED: %d failures, Avg weighted error: %.2f", fail_count,
|
|
weighted_sum / num_tests);
|
|
} else {
|
|
printf("\n\ntest PASSED! Avg weighted error: %.2f", weighted_sum / num_tests);
|
|
}
|
|
|
|
cl_assert_equal_i(fail_count, 0);
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
static const char *prv_status_str(bool passed) {
|
|
if (!passed) {
|
|
return "FAIL";
|
|
} else {
|
|
return "pass";
|
|
}
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Returns the weighted error of this test
|
|
static float prv_compute_test_error(const char *name, ExpectedValue *exp, ActualValue *act,
|
|
float weight, bool *all_passed) {
|
|
int error = 0;
|
|
float weighted_error = 0.0;
|
|
|
|
if (exp->value != -1) {
|
|
error = abs(act->value - exp->value);
|
|
weighted_error = (float)error * weight;
|
|
|
|
act->passed = (act->value >= exp->min && act->value <= exp->max);
|
|
if (!act->passed) {
|
|
*all_passed = false;
|
|
}
|
|
printf("\nRESULTS for %s: exp: (%d,%d), act: %d, error: %d, weighted_error: %f, %s",
|
|
name, (int)exp->min, (int)exp->max, (int)act->value, (int)error, weighted_error,
|
|
prv_status_str(act->passed));
|
|
} else {
|
|
act->passed = true;
|
|
printf("\nRESULTS for %s: exp: (NA), act: %d, error: NA, weighted_error: NA",
|
|
name, (int)act->value);
|
|
}
|
|
|
|
return weighted_error;
|
|
}
|
|
|
|
|
|
// =========================================================================================
|
|
// Support for capturing sleep sessions detected by the algorithm
|
|
typedef struct {
|
|
KAlgActivityType activity;
|
|
time_t start_utc;
|
|
uint16_t len_m;
|
|
} KAlgTestSleepSession;
|
|
|
|
KAlgTestSleepSession s_captured_sleep_sessions[MAX_CAPTURED_SESSIONS];
|
|
int s_num_captured_sleep_sessions;
|
|
void prv_sleep_session_callback(void *context, KAlgActivityType activity_type,
|
|
time_t start_utc, uint32_t len_sec, bool ongoing, bool delete,
|
|
uint32_t steps, uint32_t resting_calories, uint32_t active_calories,
|
|
uint32_t distance_mm) {
|
|
|
|
int entry_idx = s_num_captured_sleep_sessions;
|
|
|
|
// If not a sleep session, ignore it
|
|
if (activity_type != KAlgActivityType_Sleep && activity_type != KAlgActivityType_RestfulSleep) {
|
|
return;
|
|
}
|
|
|
|
// Look for an existing session
|
|
KAlgTestSleepSession *session = s_captured_sleep_sessions;
|
|
for (int i = 0; i < s_num_captured_sleep_sessions; i++, session++) {
|
|
if (session->start_utc == start_utc && session->activity == activity_type) {
|
|
entry_idx = i;
|
|
break;
|
|
}
|
|
}
|
|
cl_assert(entry_idx < MAX_CAPTURED_SESSIONS);
|
|
|
|
// Deleting?
|
|
if (delete) {
|
|
if (entry_idx < s_num_captured_sleep_sessions) {
|
|
int num_to_move = s_num_captured_sleep_sessions - entry_idx - 1;
|
|
cl_assert(num_to_move >= 0);
|
|
memmove(&s_captured_sleep_sessions[entry_idx], &s_captured_sleep_sessions[entry_idx + 1],
|
|
num_to_move * sizeof(KAlgTestSleepSession));
|
|
s_num_captured_sleep_sessions--;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Update/add session
|
|
s_captured_sleep_sessions[entry_idx] = (KAlgTestSleepSession) {
|
|
.activity = activity_type,
|
|
.len_m = len_sec / SECONDS_PER_MINUTE,
|
|
.start_utc = start_utc,
|
|
};
|
|
|
|
if (entry_idx == s_num_captured_sleep_sessions) {
|
|
s_num_captured_sleep_sessions++;
|
|
}
|
|
}
|
|
|
|
|
|
// --------------------------------------------------------------------------------------------
|
|
// Collect summary sleep information from a collection of sessions
|
|
static void prv_get_sleep_summary(SleepTestResults *results, time_t test_start_utc,
|
|
time_t test_end_utc, time_t last_processed_utc) {
|
|
*results = (SleepTestResults) { };
|
|
|
|
// Iterate through the sleep sessions
|
|
KAlgTestSleepSession *session = s_captured_sleep_sessions;
|
|
time_t enter_utc = 0;
|
|
time_t exit_utc = 0;
|
|
time_t deep_exit_utc = 0;
|
|
uint16_t last_session_len_m = 0;
|
|
uint16_t last_deep_session_len_m = 0;
|
|
bool first_container = true;
|
|
KAlgTestSleepSession *container_session = NULL;
|
|
for (uint32_t i = 0; i < s_num_captured_sleep_sessions; i++, session++) {
|
|
|
|
// Get info on this session
|
|
time_t session_exit_utc = session->start_utc + session->len_m * SECONDS_PER_MINUTE;
|
|
|
|
// Skip if not a sleep session
|
|
bool is_restful = false;
|
|
switch (session->activity) {
|
|
case KAlgActivityType_Sleep:
|
|
break;
|
|
case KAlgActivityType_RestfulSleep:
|
|
is_restful = true;
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
|
|
const char *desc = is_restful ? " restful" : "sleep";
|
|
printf("\nfound %s session: len: %"PRIu16" min., start: %s", desc, session->len_m,
|
|
prv_log_time(session->start_utc));
|
|
|
|
if (!is_restful) {
|
|
container_session = session;
|
|
last_session_len_m = session->len_m;
|
|
|
|
// Accumulate sleep container stats
|
|
results->total.value += session->len_m;
|
|
if (first_container || session->start_utc < enter_utc) {
|
|
enter_utc = session->start_utc;
|
|
}
|
|
if (first_container || session_exit_utc > exit_utc) {
|
|
exit_utc = session_exit_utc;
|
|
}
|
|
first_container = false;
|
|
|
|
} else {
|
|
// Insure that restful sessions are inside the previous container
|
|
cl_assert(container_session != NULL);
|
|
cl_assert(session->start_utc >= container_session->start_utc);
|
|
cl_assert(session->start_utc < container_session->start_utc
|
|
+ container_session->len_m * SECONDS_PER_MINUTE);
|
|
last_deep_session_len_m = session->len_m;
|
|
// Accumulate restful sleep stats
|
|
results->deep.value += session->len_m;
|
|
if (deep_exit_utc == 0 || session_exit_utc > deep_exit_utc) {
|
|
deep_exit_utc = session_exit_utc;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fill in the rest of the sleep data metrics
|
|
if (enter_utc != 0) {
|
|
results->start_at.value = (enter_utc - test_start_utc) / SECONDS_PER_MINUTE;
|
|
}
|
|
if (exit_utc != 0) {
|
|
results->end_at.value = (exit_utc - test_start_utc) / SECONDS_PER_MINUTE;
|
|
}
|
|
|
|
|
|
// Figure out our current state
|
|
if (exit_utc >= last_processed_utc - SECONDS_PER_MINUTE) {
|
|
// We are sleeping
|
|
results->in_sleep.value = true;
|
|
int unprocessed_m = (test_end_utc - last_processed_utc) / SECONDS_PER_MINUTE;
|
|
if (exit_utc == deep_exit_utc) {
|
|
results->in_deep_sleep.value = true;
|
|
results->cur_state_elapsed.value = last_deep_session_len_m + unprocessed_m;
|
|
} else {
|
|
results->cur_state_elapsed.value = last_session_len_m + unprocessed_m;
|
|
}
|
|
} else {
|
|
if (exit_utc != 0) {
|
|
results->cur_state_elapsed.value = (test_end_utc - exit_utc) / SECONDS_PER_MINUTE;
|
|
} else {
|
|
results->cur_state_elapsed.value = (test_end_utc - test_start_utc) / SECONDS_PER_MINUTE;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// Run a set of samples through and verify that we got the right minute data
|
|
static void prv_test_minute_data(AccelRawData *samples, int num_samples,
|
|
TestMinuteData *exp_minutes, int exp_num_minutes) {
|
|
int minute_data_len = 100;
|
|
TestMinuteData minute_data[minute_data_len];
|
|
|
|
// Run the step algorithm
|
|
prv_feed_kalg_samples(samples, num_samples, minute_data, &minute_data_len);
|
|
|
|
for (int i = 0; i < minute_data_len; i++) {
|
|
printf("\n %-4d 0x%-4x %-4d", (int)minute_data[i].steps,
|
|
(int)minute_data[i].orientation, (int)minute_data[i].vmc);
|
|
}
|
|
printf("\n");
|
|
|
|
// Verify that we got the expected minute data
|
|
cl_assert_equal_i(minute_data_len, exp_num_minutes);
|
|
for (int j = 0; j < minute_data_len; j++) {
|
|
cl_assert_equal_i(minute_data[j].steps, exp_minutes[j].steps);
|
|
cl_assert_equal_i(minute_data[j].orientation, exp_minutes[j].orientation);
|
|
cl_assert_equal_i(minute_data[j].vmc, exp_minutes[j].vmc);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
void test_kraepelin_algorithm__sleep_tests(void) {
|
|
bool success = prv_sample_discovery_init(&s_sleep_sample_discovery_state.common,
|
|
SampleFileType_MinuteSamples,
|
|
"activity/sleep_samples");
|
|
cl_assert(success);
|
|
|
|
// Init algorithm state
|
|
s_kalg_state = kernel_zalloc(kalg_state_size());
|
|
|
|
const uint32_t k_max_tests = 1000;
|
|
uint32_t num_tests = 0;
|
|
|
|
// Results
|
|
SleepTestResults test_results[k_max_tests];
|
|
memset(test_results, 0, sizeof(test_results));
|
|
|
|
// List of metrics we measure for each test
|
|
// IMPORTANT: This order must match the order in the SleepTestEntry and the SleepTestResults
|
|
const char *metrics[] = {"total", "deep", "start", "end", "elapsed", "insleep",
|
|
"indeep"};
|
|
|
|
SleepFileTestEntry test_entry[k_max_tests];
|
|
memset(test_entry, 0, sizeof(test_entry));
|
|
while (prv_sleep_sample_discovery_next(&test_entry[num_tests])) {
|
|
SleepFileTestEntry *entry = &test_entry[num_tests];
|
|
|
|
#ifdef SLEEP_TEST_ONLY
|
|
if (strcmp(entry->name, SLEEP_TEST_ONLY)) {
|
|
continue;
|
|
}
|
|
#endif
|
|
entry->test_idx = num_tests;
|
|
|
|
printf("\n\n========================================================");
|
|
printf("\nRunning sleep sample set: \"%s\"\n", entry->name);
|
|
|
|
// It's easier to understand the algorithm log messages if we start at 0 time
|
|
rtc_set_time(0);
|
|
memset(s_kalg_state, 0, kalg_state_size());
|
|
kalg_init(s_kalg_state, prv_stats_cb);
|
|
s_num_captured_sleep_sessions = 0;
|
|
|
|
// Run samples through the activity detector
|
|
time_t now = rtc_get_time();
|
|
time_t test_start_utc = now;
|
|
for (int i = 0; i < entry->num_samples; i++) {
|
|
uint16_t vmc = entry->samples[i].v5_fields.vmc;
|
|
if (entry->version == 1) {
|
|
// Convert from the old compressed VMC to the new uncompressed one
|
|
vmc = vmc * vmc * 1850 / 1250;
|
|
}
|
|
const bool shutting_down = (entry->force_shut_down_at == i);
|
|
kalg_activities_update(s_kalg_state, now, entry->samples[i].v5_fields.steps, vmc,
|
|
entry->samples[i].v5_fields.orientation,
|
|
entry->samples[i].v5_fields.plugged_in,
|
|
0 /*rest_cals*/, 0 /*active_cals*/, 0 /*distance*/, shutting_down,
|
|
prv_sleep_session_callback, NULL);
|
|
if (shutting_down) {
|
|
break;
|
|
}
|
|
|
|
now += SECONDS_PER_MINUTE;
|
|
rtc_set_time(now);
|
|
}
|
|
time_t test_end_utc = now;
|
|
time_t last_processed_utc = kalg_activity_last_processed_time(s_kalg_state,
|
|
KAlgActivityType_Sleep);
|
|
|
|
// Get summary of the sleep
|
|
SleepTestResults result = { };
|
|
prv_get_sleep_summary(&result, test_start_utc, test_end_utc, last_processed_utc);
|
|
result.weighted_err = 0.0;
|
|
result.all_passed = true;
|
|
|
|
ActualValue *actual = &result.total;
|
|
ExpectedValue *expected = &entry->total;
|
|
for (int j = 0; j < ARRAY_LENGTH(metrics); j++, actual++, expected++) {
|
|
result.weighted_err += prv_compute_test_error(
|
|
metrics[j], expected, actual, entry->weight, &result.all_passed);
|
|
}
|
|
|
|
test_results[num_tests] = result;
|
|
num_tests++;
|
|
if (num_tests >= k_max_tests) {
|
|
printf("RAN INTO MAX NUMBER OF TESTS WE SUPPORT");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Make sure we discovered at least 1 test
|
|
cl_assert(num_tests > 0);
|
|
|
|
// Let's sort the tests by name
|
|
qsort(&test_entry[0], num_tests, sizeof(test_entry[0]), prv_qsort_sleep_test_entry_cb);
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Print results in a table
|
|
printf("\n\n");
|
|
printf("\n%-24s", "name");
|
|
|
|
// Print header line
|
|
for (int i = 0; i < ARRAY_LENGTH(metrics); i++) {
|
|
printf(" exp_%-8s act_%-7s", metrics[i], metrics[i]);
|
|
}
|
|
printf(" %-10s %-10s", "weight_err", "status");
|
|
|
|
printf("\n------------------------");
|
|
for (int i = 0; i < ARRAY_LENGTH(metrics); i++) {
|
|
printf("| ---------------------- ");
|
|
}
|
|
|
|
float weighted_sum = 0.0;
|
|
int pass_count = 0;
|
|
int fail_count = 0;
|
|
SleepFileTestEntry *entry = &test_entry[0];
|
|
SleepTestResults *results;
|
|
for (int i = 0; i < num_tests; i++, entry++, results++) {
|
|
results = &test_results[entry->test_idx];
|
|
|
|
// Generate the status string
|
|
const char *status = prv_status_str(results->all_passed);
|
|
if (results->all_passed) {
|
|
pass_count++;
|
|
} else {
|
|
fail_count++;
|
|
}
|
|
|
|
// Print name of test
|
|
printf("\n%-24s", entry->name);
|
|
|
|
// Print each metric for this test
|
|
ActualValue *actual = &results->total;
|
|
ExpectedValue *expected = &entry->total;
|
|
for (int j = 0; j < ARRAY_LENGTH(metrics); j++, actual++, expected++) {
|
|
char *indicator;
|
|
if (actual->passed) {
|
|
indicator = " ";
|
|
} else {
|
|
indicator = "**";
|
|
}
|
|
if (expected->value != -1) {
|
|
int delta = actual->value - expected->value;
|
|
printf(" (%3d,%3d) %s%3d (%+4d) ", expected->min, expected->max, indicator, actual->value,
|
|
delta);
|
|
} else {
|
|
printf(" (NA, NA ) %s%3d ", indicator, actual->value);
|
|
}
|
|
}
|
|
|
|
printf(" %-10.2f %-10s", results->weighted_err, status);
|
|
weighted_sum += results->weighted_err;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Overall Summary
|
|
if (fail_count) {
|
|
printf("\n\ntest FAILED: %d failures, Avg weighted error: %.2f", fail_count,
|
|
weighted_sum / num_tests);
|
|
} else {
|
|
printf("\n\ntest PASSED! Avg weighted error: %.2f", weighted_sum / num_tests);
|
|
}
|
|
|
|
cl_assert_equal_i(fail_count, 0);
|
|
kernel_free(s_kalg_state);
|
|
s_kalg_state = NULL;
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
void test_kraepelin_algorithm__activity_tests(void) {
|
|
bool success = prv_sample_discovery_init(&s_activity_sample_discovery_state.common,
|
|
SampleFileType_MinuteSamples,
|
|
"activity/activity_samples");
|
|
cl_assert(success);
|
|
|
|
// Init algorithm state
|
|
s_kalg_state = kernel_zalloc(kalg_state_size());
|
|
|
|
const uint32_t k_max_tests = 1000;
|
|
uint32_t num_tests = 0;
|
|
|
|
// Results
|
|
ActivityTestResults test_results[k_max_tests];
|
|
memset(test_results, 0, sizeof(test_results));
|
|
|
|
// List of metrics we measure for each test
|
|
// IMPORTANT: This order must match the order in the SleepTestEntry and the SleepTestResults
|
|
const char *metrics[] = {"type", "len", "start"};
|
|
|
|
ActivityFileTestEntry test_entry[k_max_tests];
|
|
memset(test_entry, 0, sizeof(test_entry));
|
|
while (prv_activity_sample_discovery_next(&test_entry[num_tests])) {
|
|
ActivityFileTestEntry *entry = &test_entry[num_tests];
|
|
|
|
#ifdef ACTIVITY_TEST_ONLY
|
|
if (strcmp(entry->name, ACTIVITY_TEST_ONLY)) {
|
|
continue;
|
|
}
|
|
#endif
|
|
entry->test_idx = num_tests;
|
|
|
|
printf("\n\n========================================================");
|
|
printf("\nRunning activity sample set: \"%s\"\n", entry->name);
|
|
|
|
memset(s_kalg_state, 0, kalg_state_size());
|
|
kalg_init(s_kalg_state, prv_stats_cb);
|
|
s_num_captured_activity_sessions = 0;
|
|
|
|
// Run samples through the activity detector
|
|
time_t now = rtc_get_time();
|
|
time_t test_start_utc = now;
|
|
for (int i = 0; i < entry->num_samples; i++) {
|
|
const bool shutting_down = (entry->force_shut_down_at == i);
|
|
kalg_activities_update(s_kalg_state, now, entry->samples[i].v5_fields.steps, 0 /*vmc*/,
|
|
0 /*orientation*/, false /*plugged_in*/, 0 /*rest_cals*/,
|
|
0 /*active_cals*/, 0 /*distance*/, shutting_down,
|
|
prv_activity_session_callback, NULL);
|
|
if (shutting_down) {
|
|
break;
|
|
}
|
|
|
|
now += SECONDS_PER_MINUTE;
|
|
rtc_set_time(now);
|
|
}
|
|
|
|
// Get summary of the activity
|
|
ActivityTestResults result = { };
|
|
KAlgTestActivitySession *session = s_captured_activity_sessions;
|
|
bool found_activity = false;
|
|
for (uint32_t i = 0; i < s_num_captured_activity_sessions; i++, session++) {
|
|
// Skip if this is a sleep session
|
|
char *desc = "";
|
|
switch (session->activity) {
|
|
case KAlgActivityType_Sleep:
|
|
case KAlgActivityType_RestfulSleep:
|
|
continue;
|
|
case KAlgActivityType_Walk:
|
|
desc = "walk";
|
|
break;
|
|
case KAlgActivityType_Run:
|
|
desc = "run";
|
|
break;
|
|
case KAlgActivityTypeCount:
|
|
WTF;
|
|
break;
|
|
}
|
|
|
|
int start_idx = (session->start_utc - test_start_utc) / SECONDS_PER_MINUTE;
|
|
printf("\nfound %s len: %d, start: %d, ", desc, (int) session->len_minutes,
|
|
start_idx);
|
|
|
|
// Only compare the first activity found
|
|
if (!found_activity) {
|
|
result.activity_type.value = (int)session->activity;
|
|
result.len.value = (int)session->len_minutes;
|
|
result.start_at.value = start_idx;
|
|
found_activity = true;
|
|
}
|
|
}
|
|
|
|
result.weighted_err = 0.0;
|
|
result.all_passed = true;
|
|
|
|
ActualValue *actual = &result.activity_type;
|
|
ExpectedValue *expected = &entry->activity_type;
|
|
for (int j = 0; j < ARRAY_LENGTH(metrics); j++, actual++, expected++) {
|
|
result.weighted_err += prv_compute_test_error(
|
|
metrics[j], expected, actual, entry->weight, &result.all_passed);
|
|
}
|
|
|
|
test_results[num_tests] = result;
|
|
num_tests++;
|
|
if (num_tests >= k_max_tests) {
|
|
printf("RAN INTO MAX NUMBER OF TESTS WE SUPPORT");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Make sure we discovered at least 1 test
|
|
cl_assert(num_tests > 0);
|
|
|
|
// Let's sort the tests by name
|
|
qsort(&test_entry[0], num_tests, sizeof(test_entry[0]), prv_qsort_activity_test_entry_cb);
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Print results in a table
|
|
printf("\n\n");
|
|
printf("\n%-24s", "name");
|
|
|
|
// Print header line
|
|
for (int i = 0; i < ARRAY_LENGTH(metrics); i++) {
|
|
printf(" exp_%-8s act_%-7s", metrics[i], metrics[i]);
|
|
}
|
|
printf(" %-10s %-10s", "weight_err", "status");
|
|
|
|
printf("\n------------------------");
|
|
for (int i = 0; i < ARRAY_LENGTH(metrics); i++) {
|
|
printf("| ---------------------- ");
|
|
}
|
|
|
|
float weighted_sum = 0.0;
|
|
int pass_count = 0;
|
|
int fail_count = 0;
|
|
ActivityFileTestEntry *entry = &test_entry[0];
|
|
ActivityTestResults *results;
|
|
for (int i = 0; i < num_tests; i++, entry++, results++) {
|
|
results = &test_results[entry->test_idx];
|
|
|
|
// Generate the status string
|
|
const char *status = prv_status_str(results->all_passed);
|
|
if (results->all_passed) {
|
|
pass_count++;
|
|
} else {
|
|
fail_count++;
|
|
}
|
|
|
|
// Print name of test
|
|
printf("\n%-24s", entry->name);
|
|
|
|
// Print each metric for this test
|
|
ActualValue *actual = &results->activity_type;
|
|
ExpectedValue *expected = &entry->activity_type;
|
|
for (int j = 0; j < ARRAY_LENGTH(metrics); j++, actual++, expected++) {
|
|
char *indicator;
|
|
if (actual->passed) {
|
|
indicator = " ";
|
|
} else {
|
|
indicator = "**";
|
|
}
|
|
if (expected->value != -1) {
|
|
int delta = actual->value - expected->value;
|
|
printf(" (%3d,%3d) %s%3d (%+4d) ", expected->min, expected->max, indicator, actual->value,
|
|
delta);
|
|
} else {
|
|
printf(" (NA, NA ) %s%3d ", indicator, actual->value);
|
|
}
|
|
}
|
|
|
|
printf(" %-10.2f %-10s", results->weighted_err, status);
|
|
weighted_sum += results->weighted_err;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Overall Summary
|
|
if (fail_count) {
|
|
printf("\n\ntest FAILED: %d failures, Avg weighted error: %.2f", fail_count,
|
|
weighted_sum / num_tests);
|
|
} else {
|
|
printf("\n\ntest PASSED! Avg weighted error: %.2f", weighted_sum / num_tests);
|
|
}
|
|
|
|
cl_assert_equal_i(fail_count, 0);
|
|
kernel_free(s_kalg_state);
|
|
s_kalg_state = NULL;
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Test that we generate the right minute statistics
|
|
void test_kraepelin_algorithm__minute_stats(void) {
|
|
|
|
// Run the 30 step sample.
|
|
// The expected results were obtained empirically on a known good commit
|
|
{
|
|
int num_samples;
|
|
AccelRawData *samples = activity_sample_30_steps(&num_samples);
|
|
TestMinuteData exp_minutes[] = {
|
|
{
|
|
.steps = 28,
|
|
.orientation = 0x47,
|
|
.vmc = 1205,
|
|
},
|
|
};
|
|
prv_test_minute_data(samples, num_samples, exp_minutes, ARRAY_LENGTH(exp_minutes));
|
|
}
|
|
|
|
// Run the working at desk sample
|
|
// The expected results were obtained empirically on a known good commit
|
|
{
|
|
int num_samples;
|
|
AccelRawData *samples = activity_sample_working_at_desk(&num_samples);
|
|
TestMinuteData exp_minutes[] = {
|
|
{
|
|
.steps = 0,
|
|
.orientation = 0x72,
|
|
.vmc = 1787,
|
|
},
|
|
};
|
|
prv_test_minute_data(samples, num_samples, exp_minutes, ARRAY_LENGTH(exp_minutes));
|
|
}
|
|
|
|
// Run the not moving sample
|
|
// The expected results were obtained empirically on a known good commit
|
|
{
|
|
int num_samples;
|
|
AccelRawData *samples = activity_sample_not_moving(&num_samples);
|
|
TestMinuteData exp_minutes[2] = {
|
|
{
|
|
.steps = 0,
|
|
.orientation = 0x81,
|
|
.vmc = 181,
|
|
},
|
|
{
|
|
.steps = 0,
|
|
.orientation = 0x81,
|
|
.vmc = 0,
|
|
},
|
|
};
|
|
prv_test_minute_data(samples, num_samples, exp_minutes, ARRAY_LENGTH(exp_minutes));
|
|
}
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Utility for feeding in artificial walk/run activity samples into the algorithm's
|
|
// activity detector logic
|
|
static void prv_insert_artificial_activity_session(KAlgTestActivityMinute *samples, int samples_len,
|
|
KAlgTestActivitySession *session) {
|
|
time_t now = rtc_get_time();
|
|
int start_idx = ((session->start_utc - now) / SECONDS_PER_MINUTE) + 1;
|
|
int len = session->len_minutes;
|
|
|
|
cl_assert(start_idx + len < samples_len);
|
|
|
|
for (int i = start_idx; i < start_idx + len; i++) {
|
|
samples[i] = (KAlgTestActivityMinute) {
|
|
.steps = session->steps / len,
|
|
.active_calories = session->active_calories / len,
|
|
.resting_calories = session->resting_calories / len,
|
|
.distance_mm = session->distance_mm / len,
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
// Feed activity minute data into the kalg_activities_update method
|
|
static void prv_feed_activity_minutes(KAlgTestActivityMinute *samples, int samples_len) {
|
|
time_t now = rtc_get_time();
|
|
for (int i = 0; i < samples_len; i++) {
|
|
// NOTE: We feed in a significant VMC to simulate activity so that the sleep algorithm
|
|
// doesn't think we're sleeping
|
|
kalg_activities_update(s_kalg_state, now, samples[i].steps, 7000 /*vmc*/, 0 /*orientation*/,
|
|
true /*plugged_in*/, samples[i].resting_calories,
|
|
samples[i].active_calories, samples[i].distance_mm, false /* shutting_down */,
|
|
prv_activity_session_callback, NULL);
|
|
now += SECONDS_PER_MINUTE;
|
|
rtc_set_time(now);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Test that we correectly recognize walk and run activities
|
|
void test_kraepelin_algorithm__walks_and_runs(void) {
|
|
const int k_minute_data_len = 60;
|
|
const int k_minute_data_bytes = k_minute_data_len * sizeof(KAlgTestActivityMinute);
|
|
|
|
// Init state
|
|
s_kalg_state = kernel_zalloc(kalg_state_size());
|
|
kalg_init(s_kalg_state, prv_stats_cb);
|
|
|
|
KAlgTestActivityMinute minute_raw_data[k_minute_data_len];
|
|
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// Test a walk session of 20 minutes long that starts 10 minutes in
|
|
{
|
|
memset(minute_raw_data, 0, k_minute_data_bytes);
|
|
s_num_captured_activity_sessions = 0;
|
|
time_t now = rtc_get_time();
|
|
|
|
int len = 20;
|
|
KAlgTestActivitySession exp_session = {
|
|
.activity = KAlgActivityType_Walk,
|
|
.start_utc = now + 10 * SECONDS_PER_MINUTE,
|
|
.steps = len * 80, // 80 steps/min
|
|
.len_minutes = len,
|
|
.resting_calories = len * 100,
|
|
.active_calories = len * 200,
|
|
.distance_mm = len * 1000,
|
|
};
|
|
|
|
prv_insert_artificial_activity_session(minute_raw_data, k_minute_data_len, &exp_session);
|
|
prv_feed_activity_minutes(minute_raw_data, k_minute_data_len);
|
|
cl_assert_equal_i(s_num_captured_activity_sessions, 1);
|
|
ASSERT_ACTIVITY_SESSION_PRESENT(s_captured_activity_sessions, s_num_captured_activity_sessions, &exp_session);
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// Test a run session of 30 minutes long that starts 10 minutes in that has a 2 minute
|
|
// gap in the middle
|
|
{
|
|
memset(minute_raw_data, 0, k_minute_data_bytes);
|
|
s_num_captured_activity_sessions = 0;
|
|
time_t now = rtc_get_time();
|
|
|
|
int len = 30;
|
|
KAlgTestActivitySession exp_session = {
|
|
.activity = KAlgActivityType_Run,
|
|
.start_utc = now + 10 * SECONDS_PER_MINUTE,
|
|
.steps = len * 150, // 150 steps/min
|
|
.len_minutes = len,
|
|
.resting_calories = len * 100,
|
|
.active_calories = len * 200,
|
|
.distance_mm = len * 1000,
|
|
};
|
|
|
|
prv_insert_artificial_activity_session(minute_raw_data, k_minute_data_len, &exp_session);
|
|
// Insert a 3 minute rest period in the middle
|
|
for (int i = 20; i < 23; i++) {
|
|
minute_raw_data[i] = (KAlgTestActivityMinute) { };
|
|
}
|
|
exp_session.steps -= 3 * 150;
|
|
exp_session.resting_calories -= 3 * 100;
|
|
exp_session.active_calories -= 3 * 200;
|
|
exp_session.distance_mm -= 3 * 1000;
|
|
prv_feed_activity_minutes(minute_raw_data, k_minute_data_len);
|
|
|
|
cl_assert_equal_i(s_num_captured_activity_sessions, 1);
|
|
ASSERT_ACTIVITY_SESSION_PRESENT(s_captured_activity_sessions, s_num_captured_activity_sessions, &exp_session);
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// Test a short walk that should not register
|
|
{
|
|
memset(minute_raw_data, 0, k_minute_data_bytes);
|
|
s_num_captured_activity_sessions = 0;
|
|
time_t now = rtc_get_time();
|
|
|
|
int len = 5;
|
|
KAlgTestActivitySession exp_session = {
|
|
.activity = KAlgActivityType_Walk,
|
|
.start_utc = now + 10 * SECONDS_PER_MINUTE,
|
|
.steps = len * 80, // 80 steps/min
|
|
.len_minutes = len,
|
|
.resting_calories = len * 100,
|
|
.active_calories = len * 200,
|
|
.distance_mm = len * 1000,
|
|
};
|
|
|
|
prv_insert_artificial_activity_session(minute_raw_data, k_minute_data_len, &exp_session);
|
|
prv_feed_activity_minutes(minute_raw_data, k_minute_data_len);
|
|
cl_assert_equal_i(s_num_captured_activity_sessions, 0);
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// Test a walk of 20 minutes followed by a run of 20 minutes
|
|
{
|
|
memset(minute_raw_data, 0, k_minute_data_bytes);
|
|
s_num_captured_activity_sessions = 0;
|
|
time_t now = rtc_get_time();
|
|
|
|
int walk_len = 15;
|
|
KAlgTestActivitySession exp_session_walk = {
|
|
.activity = KAlgActivityType_Walk,
|
|
.start_utc = now + 5 * SECONDS_PER_MINUTE,
|
|
.steps = walk_len * 80, // 80 steps/min
|
|
.len_minutes = walk_len,
|
|
.resting_calories = walk_len * 100,
|
|
.active_calories = walk_len * 200,
|
|
.distance_mm = walk_len * 1000,
|
|
};
|
|
|
|
int run_len = 15;
|
|
KAlgTestActivitySession exp_session_run = {
|
|
.activity = KAlgActivityType_Run,
|
|
.start_utc = now + 30 * SECONDS_PER_MINUTE,
|
|
.steps = run_len * 150, // 150 steps/min
|
|
.len_minutes = run_len,
|
|
.resting_calories = run_len * 100,
|
|
.active_calories = run_len * 200,
|
|
.distance_mm = run_len * 1000,
|
|
};
|
|
|
|
prv_insert_artificial_activity_session(minute_raw_data, k_minute_data_len, &exp_session_walk);
|
|
prv_insert_artificial_activity_session(minute_raw_data, k_minute_data_len, &exp_session_run);
|
|
|
|
prv_feed_activity_minutes(minute_raw_data, k_minute_data_len);
|
|
cl_assert_equal_i(s_num_captured_activity_sessions, 2);
|
|
ASSERT_ACTIVITY_SESSION_PRESENT(s_captured_activity_sessions, s_num_captured_activity_sessions,
|
|
&exp_session_walk);
|
|
ASSERT_ACTIVITY_SESSION_PRESENT(s_captured_activity_sessions, s_num_captured_activity_sessions,
|
|
&exp_session_run);
|
|
}
|
|
|
|
kernel_free(s_kalg_state);
|
|
s_kalg_state = NULL;
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
void test_kraepelin_algorithm__sleep_stats(void) {
|
|
// Init algorithm state
|
|
s_kalg_state = kernel_zalloc(kalg_state_size());
|
|
|
|
// It's easier to understand the algorithm log messages if we start at 0 time
|
|
rtc_set_time(0);
|
|
memset(s_kalg_state, 0, kalg_state_size());
|
|
kalg_init(s_kalg_state, prv_stats_cb);
|
|
s_num_captured_sleep_sessions = 0;
|
|
|
|
// Get the samples for this test
|
|
int num_samples;
|
|
AlgMinuteFileSampleV5 *samples = activity_sample_sleep_v1_1(&num_samples);
|
|
|
|
// Run samples through the activity detector
|
|
time_t now = rtc_get_time();
|
|
time_t test_start_utc = now;
|
|
for (int i = 0; i < num_samples; i++) {
|
|
uint16_t vmc = samples[i].vmc;
|
|
// Convert from the old compressed VMC to the new uncompressed one
|
|
vmc = vmc * vmc * 1850 / 1250;
|
|
kalg_activities_update(s_kalg_state, now, samples[i].steps, vmc,
|
|
samples[i].orientation, samples[i].plugged_in,
|
|
0 /*rest_cals*/, 0 /*active_cals*/, 0 /*distance*/, false /* shutting_down */,
|
|
prv_sleep_session_callback, NULL);
|
|
|
|
// This particular sample has sleep from minute 32 to 353
|
|
const int k_sleep_start_m = 32;
|
|
const int k_sleep_end_m = 353;
|
|
const time_t k_sleep_start_utc = test_start_utc + k_sleep_start_m * SECONDS_PER_MINUTE;
|
|
|
|
KAlgOngoingSleepStats stats;
|
|
kalg_get_sleep_stats(s_kalg_state, &stats);
|
|
|
|
// If we ask for the stats before sleep starts, should be no sleep info
|
|
if (i < k_sleep_start_m) {
|
|
cl_assert_equal_i(stats.sleep_start_utc, 0);
|
|
cl_assert_equal_i(stats.sleep_len_m, 0);
|
|
cl_assert_equal_i(stats.uncertain_start_utc, 0);
|
|
}
|
|
|
|
// If we ask once we know for sure sleep has started (at least 1 hour into it)
|
|
if (i >= (k_sleep_start_m + 70) && (i <= k_sleep_end_m)) {
|
|
cl_assert_equal_i(stats.sleep_start_utc, k_sleep_start_utc);
|
|
cl_assert_equal_i(stats.sleep_len_m, i - k_sleep_start_m - KALG_MAX_UNCERTAIN_SLEEP_M);
|
|
cl_assert_equal_i((now - stats.uncertain_start_utc) / SECONDS_PER_MINUTE,
|
|
KALG_MAX_UNCERTAIN_SLEEP_M);
|
|
}
|
|
|
|
// After we're certain sleep ended
|
|
if (i > k_sleep_end_m + KALG_MAX_UNCERTAIN_SLEEP_M) {
|
|
cl_assert_equal_i(stats.sleep_start_utc, k_sleep_start_utc);
|
|
cl_assert_equal_i(stats.sleep_len_m, k_sleep_end_m - k_sleep_start_m);
|
|
cl_assert_equal_i(stats.uncertain_start_utc, 0);
|
|
}
|
|
|
|
now += SECONDS_PER_MINUTE;
|
|
rtc_set_time(now);
|
|
}
|
|
|
|
kernel_free(s_kalg_state);
|
|
s_kalg_state = NULL;
|
|
}
|
|
|
|
|