Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Fitness App (Step counting data only #362

Merged
merged 8 commits into from
Oct 24, 2024
140 changes: 136 additions & 4 deletions app/src/applications/fitness/fitness_app.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,44 @@
#include "sensors/zsw_imu.h"
#include "zsw_clock.h"
#include "zsw_alarm.h"
#include "history/zsw_history.h"
#include "ui/zsw_ui.h"
#include "zsw_clock.h"

LOG_MODULE_REGISTER(fitness_app, LOG_LEVEL_INF);

#define STEP_RESET_COUNTER_INTERVAL_S 50
#define DAYS_IN_WEEK 7

LOG_MODULE_REGISTER(fitness_app, LOG_LEVEL_DBG);
#define SETTING_BATTERY_HIST "fitness/step/hist"
#define SAMPLE_INTERVAL_MIN 60
#define SAMPLE_INTERVAL_MS (SAMPLE_INTERVAL_MIN * 60 * 1000)
#define MAX_SAMPLES (7 * 24) // One week of hourly samples

#define STEP_RESET_COUNTER_INTERVAL_S 50
typedef struct {
zsw_timeval_t time; // TODO optimize size as NVS settings backend can do < 4096 bytes per store
uint32_t steps;
} zsw_step_sample_t;

static void fitness_app_start(lv_obj_t *root, lv_group_t *group);
static void fitness_app_stop(void);

static void step_sample_work(struct k_work *work);

ZSW_LV_IMG_DECLARE(move);

static application_t app = {
.name = "Fitness",
.start_func = fitness_app_start,
.stop_func = fitness_app_stop,
.hidden = true,
.icon = ZSW_LV_IMG_USE(move),
};

static zsw_history_t fitness_history_context;
static zsw_step_sample_t samples[MAX_SAMPLES];

K_WORK_DELAYABLE_DEFINE(sample_step_work, step_sample_work);

ZBUS_CHAN_DECLARE(accel_data_chan);
#ifndef CONFIG_RTC
static void step_work_callback(struct k_work *work);
Expand Down Expand Up @@ -57,9 +80,93 @@ static void step_work_callback(struct k_work *work)
}
}

static void step_sample_work(struct k_work *work)
{
zsw_step_sample_t sample;
int next_sample_seconds;

if (zsw_imu_fetch_num_steps(&sample.steps) != 0) {
#ifdef CONFIG_BOARD_NATIVE_POSIX
sample.steps = rand() % 1000;
#else
LOG_WRN("Error during fetching of steps!");
return;
#endif
}
zsw_clock_get_time(&sample.time);
zsw_history_add(&fitness_history_context, &sample);
if (zsw_history_save(&fitness_history_context)) {
LOG_ERR("Error during saving of step samples!");
}
LOG_DBG("Step sample hist add: %d", sample.steps);
LOG_DBG("Time: %d:%d:%d", sample.time.tm.tm_hour, sample.time.tm.tm_min, sample.time.tm.tm_sec);
next_sample_seconds = 60 * (SAMPLE_INTERVAL_MIN - sample.time.tm.tm_min) - sample.time.tm.tm_sec;
LOG_DBG("Next sample in %d:%d", next_sample_seconds / 60, next_sample_seconds % 60);
k_work_reschedule(&sample_step_work, K_SECONDS(next_sample_seconds));
}

static void get_steps_per_day(uint16_t weekdays[DAYS_IN_WEEK])
{
int day;
int num_samples = zsw_history_samples(&fitness_history_context);

for (int i = 0; i < num_samples; i++) {
zsw_step_sample_t sample;
zsw_history_get(&fitness_history_context, &sample, i);
day = sample.time.tm.tm_wday;
LOG_DBG("Day: %d, HH: %d, Steps: %d", day, sample.time.tm.tm_hour, sample.steps);
weekdays[day] = MAX(sample.steps, weekdays[day]);
}
}

static void shift_array_n_left(uint16_t *arr, int n, int size)
{
for (int i = 0; i < n; i++) {
int first = arr[0];
for (int j = 0; j < size - 1; j++) {
arr[j] = arr[j + 1];
}
arr[size - 1] = first;
}
}

static void shift_char_array_n_left(char **arr, int n, int size)
{
for (int i = 0; i < n; i++) {
char *first = arr[0];
for (int j = 0; j < size - 1; j++) {
arr[j] = arr[j + 1];
}
arr[size - 1] = first;
}
}

static void fitness_app_start(lv_obj_t *root, lv_group_t *group)
{
fitness_ui_show(root);
zsw_timeval_t time;
uint32_t steps;
uint16_t step_weekdays[DAYS_IN_WEEK] = {0};
static char *weekday_names[] = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"};
zsw_clock_get_time(&time);
get_steps_per_day(step_weekdays);

// Data is in the order of the days of the week, starting from Sunday (index 0)
// Rotate the array (left/counter-clockwise) so the last element is the current day
// As we want the last bar in the chart to be "Today".
int shifts = time.tm.tm_wday + 1;
shift_array_n_left(step_weekdays, shifts, DAYS_IN_WEEK);
shift_char_array_n_left(weekday_names, shifts, DAYS_IN_WEEK);

for (int i = 0; i < DAYS_IN_WEEK; i++) {
LOG_DBG("%s %d: %d\n", weekday_names[i], i, step_weekdays[i]);
}

fitness_ui_show(root, DAYS_IN_WEEK);
fitness_ui_set_weekly_steps(step_weekdays, weekday_names, DAYS_IN_WEEK);

if (zsw_imu_fetch_num_steps(&steps) == 0) {
fitness_ui_set_daily_steps(steps);
}
}

static void fitness_app_stop(void)
Expand All @@ -69,6 +176,9 @@ static void fitness_app_stop(void)

static int fitness_app_add(void)
{
int num_hist_samples;
zsw_timeval_t time;
int next_sample_seconds = 0;
zsw_app_manager_add_application(&app);

#ifdef CONFIG_RTC
Expand All @@ -82,6 +192,28 @@ static int fitness_app_add(void)
k_work_reschedule(&step_work, K_SECONDS(STEP_RESET_COUNTER_INTERVAL_S));
#endif

zsw_history_init(&fitness_history_context, MAX_SAMPLES, sizeof(zsw_step_sample_t), samples, SETTING_BATTERY_HIST);

if (zsw_history_load(&fitness_history_context)) {
LOG_ERR("Error during settings_load_subtree!");
return -EFAULT;
}

num_hist_samples = zsw_history_samples(&fitness_history_context);

zsw_clock_get_time(&time);

// If watch was reset the step counter restarts at 0, so we need to update the offset.
if (num_hist_samples > 0 && time.tm.tm_mday == samples[num_hist_samples - 1].time.tm.tm_mday) {
zsw_imu_set_step_offset(samples[num_hist_samples - 1].steps);
}

// Try to sample about every full hour
next_sample_seconds = 60 * (SAMPLE_INTERVAL_MIN - time.tm.tm_min) - time.tm.tm_sec;

LOG_DBG("Next sample in %d:%d", next_sample_seconds / 60, next_sample_seconds % 60);
k_work_reschedule(&sample_step_work, K_SECONDS(next_sample_seconds));

return 0;
}

Expand Down
201 changes: 200 additions & 1 deletion app/src/applications/fitness/fitness_ui.c
Original file line number Diff line number Diff line change
@@ -1,9 +1,184 @@
#include "fitness_ui.h"
#include "ui/zsw_ui.h"
#include <lvgl.h>

static lv_obj_t *root_page = NULL;
static lv_obj_t *ui_step_progress_label = NULL;
static lv_obj_t *ui_weekly_chart = NULL;
static lv_obj_t *ui_step_goal_arc = NULL;
static lv_chart_series_t *ui_weekly_chart_series_1 = NULL;
static char **chart_bar_names = NULL;

void fitness_ui_show(lv_obj_t *root)
static void event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t *chart = lv_event_get_target(e);

if (code == LV_EVENT_VALUE_CHANGED) {
lv_obj_invalidate(chart);
} else if (code == LV_EVENT_DRAW_POST_END) {
lv_chart_series_t *ser = lv_chart_get_series_next(chart, NULL);
if (!ser) {
return;
}
int num_points = lv_chart_get_point_count(chart);

for (int id = 0; id < num_points; id++) {
lv_point_t p;
lv_chart_get_point_pos_by_id(chart, ser, id, &p);

char buf[16];
lv_snprintf(buf, sizeof(buf), "%s", chart_bar_names[id]);

// Draw the day of week above each bar
lv_draw_label_dsc_t draw_label_dsc;
lv_draw_label_dsc_init(&draw_label_dsc);
draw_label_dsc.color = zsw_color_red();
draw_label_dsc.align = LV_TEXT_ALIGN_CENTER;

lv_area_t a;
a.x1 = chart->coords.x1 + p.x - 15;
a.x2 = chart->coords.x1 + p.x + 17;
a.y1 = chart->coords.y1 + 20 - 30;
a.y2 = chart->coords.y1 + 20 - 10;

lv_draw_ctx_t *draw_ctx = lv_event_get_draw_ctx(e);
lv_draw_label(draw_ctx, &draw_label_dsc, &a, buf, NULL);

// Draw a vertical line to separate the bars
lv_draw_rect_dsc_t draw_rect_dsc;
lv_draw_rect_dsc_init(&draw_rect_dsc);
draw_rect_dsc.bg_opa = LV_OPA_TRANSP;
draw_rect_dsc.border_color = zsw_color_gray();
draw_rect_dsc.border_width = 1;
draw_rect_dsc.border_side = LV_BORDER_SIDE_RIGHT;

// Note thise values are not dynamic, so needs change if graph size changes.
a.y1 = chart->coords.y1 + 20 - 30;
a.y2 = chart->coords.y1 + 20 + 99;

// Don't draw a line after the last bar
if (id != num_points - 1) {
lv_draw_rect(draw_ctx, &draw_rect_dsc, &a);
}
}

static int32_t id = LV_CHART_POINT_NONE;
if (lv_chart_get_pressed_point(chart) != LV_CHART_POINT_NONE) {
id = lv_chart_get_pressed_point(chart);
}
if (id != LV_CHART_POINT_NONE) {
// Draw a rectangle with the value of the point
// Keep point value drawn until another bar is clicked.
lv_point_t p;
lv_chart_get_point_pos_by_id(chart, ser, id, &p);

lv_coord_t *y_array = lv_chart_get_y_array(chart, ser);
lv_coord_t value = y_array[id];

char buf[16];
lv_snprintf(buf, sizeof(buf), LV_SYMBOL_DUMMY"%d", value);

lv_draw_rect_dsc_t draw_rect_dsc;
lv_draw_rect_dsc_init(&draw_rect_dsc);
draw_rect_dsc.bg_color = lv_color_white();
draw_rect_dsc.bg_opa = LV_OPA_70;
draw_rect_dsc.radius = 5;
draw_rect_dsc.bg_img_src = buf;
draw_rect_dsc.bg_img_recolor = lv_color_black();

lv_area_t a;
a.x1 = chart->coords.x1 + p.x - 20;
a.x2 = chart->coords.x1 + p.x + 20;
a.y1 = chart->coords.y1 + p.y - 30;
a.y2 = chart->coords.y1 + p.y - 10;

lv_draw_ctx_t *draw_ctx = lv_event_get_draw_ctx(e);
lv_draw_rect(draw_ctx, &draw_rect_dsc, &a);
}
}
}

static void create_step_chart(lv_obj_t *ui_root_container, uint16_t max_samples)
{
ui_step_progress_label = lv_label_create(ui_root_container);
lv_obj_set_width(ui_step_progress_label, LV_SIZE_CONTENT); /// 1
lv_obj_set_height(ui_step_progress_label, LV_SIZE_CONTENT); /// 1
lv_obj_set_x(ui_step_progress_label, 0);
lv_obj_set_y(ui_step_progress_label, -90);
lv_obj_set_align(ui_step_progress_label, LV_ALIGN_CENTER);
lv_obj_set_style_text_color(ui_step_progress_label, zsw_color_blue(), LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_text_font(ui_step_progress_label, &lv_font_montserrat_18, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_label_set_text(ui_step_progress_label, "- / 10000");

ui_weekly_chart = lv_chart_create(ui_root_container);
lv_obj_set_width(ui_weekly_chart, 231);
lv_obj_set_height(ui_weekly_chart, 120);
lv_obj_set_x(ui_weekly_chart, 0);
lv_obj_set_y(ui_weekly_chart, 10);
lv_obj_set_align(ui_weekly_chart, LV_ALIGN_CENTER);
lv_chart_set_type(ui_weekly_chart, LV_CHART_TYPE_BAR);
lv_chart_set_point_count(ui_weekly_chart, 7);
lv_chart_set_range(ui_weekly_chart, LV_CHART_AXIS_PRIMARY_Y, 0, 12000);
lv_chart_set_range(ui_weekly_chart, LV_CHART_AXIS_SECONDARY_Y, 0, 0);
lv_chart_set_div_line_count(ui_weekly_chart, 0, 0);
lv_chart_set_axis_tick(ui_weekly_chart, LV_CHART_AXIS_PRIMARY_X, 1, 1, 1, 1, false, 50);
lv_chart_set_axis_tick(ui_weekly_chart, LV_CHART_AXIS_PRIMARY_Y, 1, 1, 1, 1, false, 50);
lv_chart_set_axis_tick(ui_weekly_chart, LV_CHART_AXIS_SECONDARY_Y, 1, 1, 1, 1, false, 25);
ui_weekly_chart_series_1 = lv_chart_add_series(ui_weekly_chart, zsw_color_blue(),
LV_CHART_AXIS_PRIMARY_Y);

lv_obj_set_style_bg_color(ui_weekly_chart, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(ui_weekly_chart, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_color(ui_weekly_chart, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_opa(ui_weekly_chart, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_width(ui_weekly_chart, 0, LV_PART_MAIN | LV_STATE_DEFAULT);

ui_step_goal_arc = lv_arc_create(ui_root_container);
lv_obj_set_width(ui_step_goal_arc, lv_pct(100));
lv_obj_set_height(ui_step_goal_arc, lv_pct(100));
lv_obj_set_align(ui_step_goal_arc, LV_ALIGN_CENTER);
lv_obj_clear_flag(ui_step_goal_arc, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_PRESS_LOCK |
LV_OBJ_FLAG_CLICK_FOCUSABLE); /// Flags
lv_arc_set_range(ui_step_goal_arc, 0, 10000);
lv_arc_set_value(ui_step_goal_arc, 2000);
lv_arc_set_bg_angles(ui_step_goal_arc, 0, 359);
lv_arc_set_rotation(ui_step_goal_arc, 270);
lv_obj_set_style_pad_left(ui_step_goal_arc, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_right(ui_step_goal_arc, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_top(ui_step_goal_arc, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_bottom(ui_step_goal_arc, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_row(ui_step_goal_arc, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_column(ui_step_goal_arc, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_arc_color(ui_step_goal_arc, lv_color_hex(0x5F6571), LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_arc_opa(ui_step_goal_arc, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_arc_width(ui_step_goal_arc, 4, LV_PART_MAIN | LV_STATE_DEFAULT);

lv_obj_set_style_pad_left(ui_step_goal_arc, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT);
lv_obj_set_style_pad_right(ui_step_goal_arc, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT);
lv_obj_set_style_pad_top(ui_step_goal_arc, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT);
lv_obj_set_style_pad_bottom(ui_step_goal_arc, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT);
lv_obj_set_style_pad_row(ui_step_goal_arc, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT);
lv_obj_set_style_pad_column(ui_step_goal_arc, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT);
lv_obj_set_style_arc_color(ui_step_goal_arc, lv_color_hex(0xffd147), LV_PART_INDICATOR | LV_STATE_DEFAULT);
lv_obj_set_style_arc_opa(ui_step_goal_arc, 255, LV_PART_INDICATOR | LV_STATE_DEFAULT);
lv_obj_set_style_arc_width(ui_step_goal_arc, 4, LV_PART_INDICATOR | LV_STATE_DEFAULT);

lv_obj_set_style_bg_color(ui_step_goal_arc, lv_color_hex(0xFFFFFF), LV_PART_KNOB | LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(ui_step_goal_arc, 0, LV_PART_KNOB | LV_STATE_DEFAULT);

lv_obj_t *ui_last_7_days_title_label = lv_label_create(ui_root_container);
lv_obj_set_width(ui_last_7_days_title_label, LV_SIZE_CONTENT); /// 1
lv_obj_set_height(ui_last_7_days_title_label, LV_SIZE_CONTENT); /// 1
lv_obj_set_x(ui_last_7_days_title_label, 0);
lv_obj_set_y(ui_last_7_days_title_label, -20);
lv_obj_set_align(ui_last_7_days_title_label, LV_ALIGN_BOTTOM_MID);
lv_label_set_text(ui_last_7_days_title_label, "Steps per day");

lv_obj_add_event_cb(ui_weekly_chart, event_cb, LV_EVENT_ALL, NULL);
}

void fitness_ui_show(lv_obj_t *root, uint16_t max_samples)
{
assert(root_page == NULL);

Expand All @@ -14,6 +189,30 @@ void fitness_ui_show(lv_obj_t *root)
lv_obj_set_width(root_page, lv_pct(100));
lv_obj_set_height(root_page, lv_pct(100));
lv_obj_set_align(root_page, LV_ALIGN_CENTER);
lv_obj_set_style_pad_all(root_page, 0, LV_PART_MAIN);

create_step_chart(root_page, max_samples);
}

void fitness_ui_set_weekly_steps(uint16_t *samples, char **weekday_names, uint16_t num_samples)
{
assert(ui_weekly_chart_series_1 != NULL);

chart_bar_names = weekday_names;

for (int i = 0; i < num_samples; i++) {
lv_chart_set_next_value(ui_weekly_chart, ui_weekly_chart_series_1, samples[i]);
}
lv_label_set_text_fmt(ui_step_progress_label, "%d / %d", samples[num_samples - 1], 10000);
lv_obj_set_style_text_align(ui_step_progress_label, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_arc_set_value(ui_step_goal_arc, samples[num_samples - 1]);
}

void fitness_ui_set_daily_steps(uint32_t steps)
{
lv_label_set_text_fmt(ui_step_progress_label, "%d / %d", steps, 10000);
lv_obj_set_style_text_align(ui_step_progress_label, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_arc_set_value(ui_step_goal_arc, steps);
}

void fitness_ui_remove(void)
Expand Down
Loading