From ea47bf9f22af080a75c00743cc14eeeb4e87e723 Mon Sep 17 00:00:00 2001 From: Matheus Afonso Martins Moreira Date: Wed, 21 Feb 2024 06:00:18 -0300 Subject: faces/pulsometer: implement advanced pulsometer Implements an advanced pulsometer that can be recalibrated by the user. The main clock face now displays the measured pulses per minute. The day of month digits now display the pulsometer calibration. The light button now cycles through integer graduations which now range from 1 to 39 pulses per minute. Long presses of the light button cycle by 10 instead of 1. The watch face's responsiveness to input has been carefully optimized. The code has been reorganized and generally improved. --- .../watch_faces/complication/pulsometer_face.c | 167 +++++++++++++++------ .../watch_faces/complication/pulsometer_face.h | 3 +- 2 files changed, 125 insertions(+), 45 deletions(-) diff --git a/movement/watch_faces/complication/pulsometer_face.c b/movement/watch_faces/complication/pulsometer_face.c index 2247421c..30eb5bdd 100644 --- a/movement/watch_faces/complication/pulsometer_face.c +++ b/movement/watch_faces/complication/pulsometer_face.c @@ -24,73 +24,152 @@ #include #include + #include "pulsometer_face.h" #include "watch.h" -#define PULSOMETER_FACE_FREQUENCY_FACTOR (4ul) // refresh rate will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.) +#ifndef PULSOMETER_FACE_TITLE +#define PULSOMETER_FACE_TITLE "PL" +#endif + +#ifndef PULSOMETER_FACE_CALIBRATION_DEFAULT +#define PULSOMETER_FACE_CALIBRATION_DEFAULT (30) +#endif + +#ifndef PULSOMETER_FACE_CALIBRATION_INCREMENT +#define PULSOMETER_FACE_CALIBRATION_INCREMENT (10) +#endif + +// tick frequency will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.) +#ifndef PULSOMETER_FACE_FREQUENCY_FACTOR +#define PULSOMETER_FACE_FREQUENCY_FACTOR (4ul) +#endif + #define PULSOMETER_FACE_FREQUENCY (1 << PULSOMETER_FACE_FREQUENCY_FACTOR) +static void pulsometer_display_title(pulsometer_state_t *pulsometer) { + watch_display_string(PULSOMETER_FACE_TITLE, 0); +} + +static void pulsometer_display_calibration(pulsometer_state_t *pulsometer) { + char buf[3]; + snprintf(buf, sizeof(buf), "%2hhd", pulsometer->calibration); + watch_display_string(buf, 2); +} + +static void pulsometer_display_measurement(pulsometer_state_t *pulsometer) { + char buf[7]; + snprintf(buf, sizeof(buf), "%-6hd", pulsometer->pulses); + watch_display_string(buf, 4); +} + +static void pulsometer_indicate(pulsometer_state_t *pulsometer) { + if (pulsometer->measuring) { + watch_set_indicator(WATCH_INDICATOR_LAP); + } else { + watch_clear_indicator(WATCH_INDICATOR_LAP); + } +} + +static void pulsometer_start_measurement(pulsometer_state_t *pulsometer) { + pulsometer->measuring = true; + pulsometer->pulses = INT16_MAX; + pulsometer->ticks = 0; + + pulsometer_indicate(pulsometer); + + movement_request_tick_frequency(PULSOMETER_FACE_FREQUENCY); +} + +static void pulsometer_measure(pulsometer_state_t *pulsometer) { + if (!pulsometer->measuring) { return; } + + pulsometer->ticks++; + + float ticks_per_minute = 60 << PULSOMETER_FACE_FREQUENCY_FACTOR; + float pulses_while_button_held = ticks_per_minute / pulsometer->ticks; + float calibrated_pulses = pulses_while_button_held * pulsometer->calibration; + calibrated_pulses += 0.5f; + + pulsometer->pulses = (int16_t) calibrated_pulses; + + pulsometer_display_measurement(pulsometer); +} + +static void pulsometer_stop_measurement(pulsometer_state_t *pulsometer) { + movement_request_tick_frequency(1); + + pulsometer->measuring = false; + + pulsometer_display_measurement(pulsometer); + pulsometer_indicate(pulsometer); +} + +static void pulsometer_cycle_calibration(pulsometer_state_t *pulsometer, int8_t increment) { + if (pulsometer->measuring) { return; } + + if (pulsometer->calibration <= 0) { + pulsometer->calibration = 1; + } + + int8_t last = pulsometer->calibration; + pulsometer->calibration += increment; + + if (pulsometer->calibration > 39) { + pulsometer->calibration = last == 39? 1 : 39; + } + + pulsometer_display_calibration(pulsometer); +} + void pulsometer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { (void) settings; (void) watch_face_index; - if (*context_ptr == NULL) *context_ptr = malloc(sizeof(pulsometer_state_t)); + + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(pulsometer_state_t)); + } } void pulsometer_face_activate(movement_settings_t *settings, void *context) { (void) settings; - memset(context, 0, sizeof(pulsometer_state_t)); + + pulsometer_state_t *pulsometer = context; + + pulsometer->calibration = PULSOMETER_FACE_CALIBRATION_DEFAULT; + pulsometer->measuring = false; + pulsometer->pulses = 0; + pulsometer->ticks = 0; + + pulsometer_display_title(pulsometer); + pulsometer_display_calibration(pulsometer); + pulsometer_display_measurement(pulsometer); } bool pulsometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { (void) settings; - pulsometer_state_t *pulsometer_state = (pulsometer_state_t *)context; - char buf[14]; + + pulsometer_state_t *pulsometer = (pulsometer_state_t *) context; + switch (event.event_type) { case EVENT_ALARM_BUTTON_DOWN: - pulsometer_state->measuring = true; - pulsometer_state->pulse = 0xFFFF; - pulsometer_state->ticks = 0; - movement_request_tick_frequency(PULSOMETER_FACE_FREQUENCY); + pulsometer_start_measurement(pulsometer); break; case EVENT_ALARM_BUTTON_UP: case EVENT_ALARM_LONG_UP: - pulsometer_state->measuring = false; - movement_request_tick_frequency(1); + pulsometer_stop_measurement(pulsometer); break; case EVENT_TICK: - if (pulsometer_state->pulse == 0 && !pulsometer_state->measuring) { - switch (pulsometer_state->ticks % 5) { - case 0: - watch_display_string(" Hold ", 2); - break; - case 1: - watch_display_string(" Alarn", 4); - break; - case 2: - watch_display_string("* Count ", 0); - break; - case 3: - watch_display_string(" 30Beats ", 0); - break; - case 4: - watch_clear_display(); - break; - } - pulsometer_state->ticks = (pulsometer_state->ticks + 1) % 5; - } else { - if (pulsometer_state->measuring && pulsometer_state->ticks) { - pulsometer_state->pulse = (int16_t)((30.0 * ((float)(60 << PULSOMETER_FACE_FREQUENCY_FACTOR) / (float)pulsometer_state->ticks)) + 0.5); - } - if (pulsometer_state->pulse > 240) { - watch_display_string(" Hi", 0); - } else if (pulsometer_state->pulse < 40) { - watch_display_string(" Lo", 0); - } else { - sprintf(buf, " %-3dbpn", pulsometer_state->pulse); - watch_display_string(buf, 0); - } - if (pulsometer_state->measuring) pulsometer_state->ticks++; - } + pulsometer_measure(pulsometer); + break; + case EVENT_LIGHT_BUTTON_UP: + pulsometer_cycle_calibration(pulsometer, 1); + break; + case EVENT_LIGHT_LONG_UP: + pulsometer_cycle_calibration(pulsometer, PULSOMETER_FACE_CALIBRATION_INCREMENT); + break; + case EVENT_LIGHT_BUTTON_DOWN: + // Inhibit the LED break; case EVENT_TIMEOUT: movement_move_to_face(0); diff --git a/movement/watch_faces/complication/pulsometer_face.h b/movement/watch_faces/complication/pulsometer_face.h index 288b62c4..61294d42 100644 --- a/movement/watch_faces/complication/pulsometer_face.h +++ b/movement/watch_faces/complication/pulsometer_face.h @@ -56,8 +56,9 @@ typedef struct { bool measuring; - int16_t pulse; + int16_t pulses; int16_t ticks; + int8_t calibration; } pulsometer_state_t; void pulsometer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); -- cgit v1.2.3 From 4b67ef56c643668513f5ac02509573fbeed415ed Mon Sep 17 00:00:00 2001 From: Matheus Afonso Martins Moreira Date: Wed, 21 Feb 2024 06:05:00 -0300 Subject: faces/pulsometer: document the advanced pulsometer Thoroughly document the new advanced pulsometer watch face by describing what it is and how it works. --- .../watch_faces/complication/pulsometer_face.h | 56 +++++++++++++--------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/movement/watch_faces/complication/pulsometer_face.h b/movement/watch_faces/complication/pulsometer_face.h index 61294d42..b5c87db7 100644 --- a/movement/watch_faces/complication/pulsometer_face.h +++ b/movement/watch_faces/complication/pulsometer_face.h @@ -28,28 +28,40 @@ /* * PULSOMETER face * - * The Pulsometer is an implementation of a sort of a classic mechanical - * watch complication. A classic pulsometer complication involves a - * chronograph with a scale calibrated for counting a certain number of - * heartbeats (often 30). You start it and begin counting heartbeats, and - * stop it after counting the specified number of beats. Once stopped, - * the needle will point to your heart rate. - * - * The pulsometer on Sensor Watch flashes its instructions at launch: - * “Hold Alarm + count 30 beats.” Using the hand on the side where you wear - * your watch, touch your carotid artery (in your neck) and feel for your - * pulse. Once you find it, use your other hand to press and hold the Alarm - * button, and count your heartbeats. When you reach 30 beats, release the - * Alarm button. The display will show a number such as “60 bpm”; this is - * your heart rate in beats per minute. - * - * Two notes: - * o For the first few seconds of a measurement, the display will read “Hi”. - * This indicates that it’s too early for the measured value to be a valid - * heart rate. Once the measurement is below 240 bpm, the display will update. - * o If you hold the button down for more than 45 seconds, the display will - * read “Lo”. If it took this long for you to count 30 heartbeats, this - * indicates that your heart rate is below 40 beats per minute. + * The pulsometer implements a classic mechanical watch complication. + * A mechanical pulsometer involves a chronograph with a scale that + * allows the user to compute the number of heart beats per minute + * in less time. The scale is calibrated, or graduated, for a fixed + * number of heart beats, most often 30. The user starts the chronograph + * and simultaneously begins counting the heart beats. The movement of + * the chronograph's seconds hand over time automatically performs the + * computations required. When the calibrated number of heart beats + * is reached, the chronograph is stopped and the seconds hand shows + * the heart rate. + * + * The Sensor Watch pulsometer improves this design with user calibration: + * it can be graduated to any value between 1 and 39 pulsations per minute. + * The default is still 30, mirroring the classic pulsometer calibration. + * This feature allows the user to reconfigure the pulsometer to count + * many other types of periodic minutely events, making it more versatile. + * For example, it can be set to 5 respirations per minute to turn it into + * an asthmometer, a nearly identical mechanical watch complication + * that doctors might use to quickly measure respiratory rate. + * + * To use the pulsometer, hold the ALARM button and count the pulses. + * When the calibrated number of pulses is reached, release the button. + * The display will show the number of pulses per minute. + * + * In order to measure heart rate, feel for a pulse using the hand with + * the watch while holding the button down with the other. + * The pulse can be easily felt on the carotid artery of the neck. + * + * In order to measure breathing rate, simply hold the ALARM button + * and count the number of breaths. + * + * To calibrate the pulsometer, press LIGHT + * to cycle to the next integer calibration. + * Long press LIGHT to cycle it by 10. */ #include "movement.h" -- cgit v1.2.3 From 4d7727323892bcf396a6faebec6d0432c7e1ec3f Mon Sep 17 00:00:00 2001 From: Matheus Afonso Martins Moreira Date: Wed, 21 Feb 2024 06:11:21 -0300 Subject: faces/pulsometer: update copyrights and credits Update the copyrights to include full name attribution to all who contributed to the pulsometer watch face, including myself. Also add an SPDX license identifier header comment to the files. --- movement/watch_faces/complication/pulsometer_face.c | 6 +++++- movement/watch_faces/complication/pulsometer_face.h | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/movement/watch_faces/complication/pulsometer_face.c b/movement/watch_faces/complication/pulsometer_face.c index 30eb5bdd..5e0209ba 100644 --- a/movement/watch_faces/complication/pulsometer_face.c +++ b/movement/watch_faces/complication/pulsometer_face.c @@ -1,7 +1,11 @@ +/* SPDX-License-Identifier: MIT */ + /* * MIT License * - * Copyright (c) 2022 Joey Castillo + * Copyright © 2021-2022 Joey Castillo + * Copyright © 2023 Jeremy O'Brien + * Copyright © 2024 Matheus Afonso Martins Moreira (https://www.matheusmoreira.com/) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/movement/watch_faces/complication/pulsometer_face.h b/movement/watch_faces/complication/pulsometer_face.h index b5c87db7..ba629f2a 100644 --- a/movement/watch_faces/complication/pulsometer_face.h +++ b/movement/watch_faces/complication/pulsometer_face.h @@ -1,7 +1,12 @@ +/* SPDX-License-Identifier: MIT */ + /* * MIT License * - * Copyright (c) 2022 Joey Castillo + * Copyright © 2021-2022 Joey Castillo + * Copyright © 2022 Alexsander Akers + * Copyright © 2023 Alex Utter + * Copyright © 2024 Matheus Afonso Martins Moreira (https://www.matheusmoreira.com/) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal -- cgit v1.2.3 From 30ebf4743e3693bc0740a479e9fa6460804fede4 Mon Sep 17 00:00:00 2001 From: Matheus Afonso Martins Moreira Date: Sat, 24 Feb 2024 05:17:56 -0300 Subject: faces/pulsometer: move structure definition Instances of the pulsometer state structure are only passed to the pulsometer itself and only via the opaque context pointer. No other code uses it. There is no need to expose it in a header file so make it an implementation detail of the watch face. --- movement/watch_faces/complication/pulsometer_face.c | 7 +++++++ movement/watch_faces/complication/pulsometer_face.h | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/movement/watch_faces/complication/pulsometer_face.c b/movement/watch_faces/complication/pulsometer_face.c index 5e0209ba..8c5f1cd6 100644 --- a/movement/watch_faces/complication/pulsometer_face.c +++ b/movement/watch_faces/complication/pulsometer_face.c @@ -51,6 +51,13 @@ #define PULSOMETER_FACE_FREQUENCY (1 << PULSOMETER_FACE_FREQUENCY_FACTOR) +typedef struct { + bool measuring; + int16_t pulses; + int16_t ticks; + int8_t calibration; +} pulsometer_state_t; + static void pulsometer_display_title(pulsometer_state_t *pulsometer) { watch_display_string(PULSOMETER_FACE_TITLE, 0); } diff --git a/movement/watch_faces/complication/pulsometer_face.h b/movement/watch_faces/complication/pulsometer_face.h index ba629f2a..5c1dae91 100644 --- a/movement/watch_faces/complication/pulsometer_face.h +++ b/movement/watch_faces/complication/pulsometer_face.h @@ -71,13 +71,6 @@ #include "movement.h" -typedef struct { - bool measuring; - int16_t pulses; - int16_t ticks; - int8_t calibration; -} pulsometer_state_t; - void pulsometer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); void pulsometer_face_activate(movement_settings_t *settings, void *context); bool pulsometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context); -- cgit v1.2.3 From e1b1493894dac8ba7730bb7ab6e97efd85f6e3ef Mon Sep 17 00:00:00 2001 From: Matheus Afonso Martins Moreira Date: Tue, 5 Mar 2024 04:10:41 -0300 Subject: faces/pulsometer: remember pulsometer calibration Avoid resetting it to default when the face is activated. Set the default pulsometer calibration once, only when the face is first set up. This makes it remember the calibration set by the user. It will no longer overwrite it. --- movement/watch_faces/complication/pulsometer_face.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/movement/watch_faces/complication/pulsometer_face.c b/movement/watch_faces/complication/pulsometer_face.c index 8c5f1cd6..bf7027ca 100644 --- a/movement/watch_faces/complication/pulsometer_face.c +++ b/movement/watch_faces/complication/pulsometer_face.c @@ -138,7 +138,9 @@ void pulsometer_face_setup(movement_settings_t *settings, uint8_t watch_face_ind (void) watch_face_index; if (*context_ptr == NULL) { - *context_ptr = malloc(sizeof(pulsometer_state_t)); + pulsometer_state_t *pulsometer = malloc(sizeof(pulsometer_state_t)); + pulsometer->calibration = PULSOMETER_FACE_CALIBRATION_DEFAULT; + *context_ptr = pulsometer; } } @@ -147,7 +149,6 @@ void pulsometer_face_activate(movement_settings_t *settings, void *context) { pulsometer_state_t *pulsometer = context; - pulsometer->calibration = PULSOMETER_FACE_CALIBRATION_DEFAULT; pulsometer->measuring = false; pulsometer->pulses = 0; pulsometer->ticks = 0; -- cgit v1.2.3 From fb3b96c8b772948956819374991e984998bd2324 Mon Sep 17 00:00:00 2001 From: Matheus Afonso Martins Moreira Date: Tue, 5 Mar 2024 04:29:52 -0300 Subject: faces/pulsometer: remember pulsometer measurement Avoid resetting it to zero when the face is activated. Initialize the variables once when the face is first set up. This makes it remember the last measurement taken by the user. It will no longer be overwritten when the watch face activates. --- movement/watch_faces/complication/pulsometer_face.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/movement/watch_faces/complication/pulsometer_face.c b/movement/watch_faces/complication/pulsometer_face.c index bf7027ca..3c04aa1a 100644 --- a/movement/watch_faces/complication/pulsometer_face.c +++ b/movement/watch_faces/complication/pulsometer_face.c @@ -139,7 +139,11 @@ void pulsometer_face_setup(movement_settings_t *settings, uint8_t watch_face_ind if (*context_ptr == NULL) { pulsometer_state_t *pulsometer = malloc(sizeof(pulsometer_state_t)); + pulsometer->calibration = PULSOMETER_FACE_CALIBRATION_DEFAULT; + pulsometer->pulses = 0; + pulsometer->ticks = 0; + *context_ptr = pulsometer; } } @@ -150,8 +154,6 @@ void pulsometer_face_activate(movement_settings_t *settings, void *context) { pulsometer_state_t *pulsometer = context; pulsometer->measuring = false; - pulsometer->pulses = 0; - pulsometer->ticks = 0; pulsometer_display_title(pulsometer); pulsometer_display_calibration(pulsometer); -- cgit v1.2.3