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