Face for tracking the menstrual cycle (#250)
Authored-by: jokomo <jokomo@parallels-ubuntu18.04>
This commit is contained in:
		
							parent
							
								
									3634460a02
								
							
						
					
					
						commit
						0f5defe789
					
				| @ -119,6 +119,7 @@ SRCS += \ | ||||
|   ../watch_faces/complication/toss_up_face.c \
 | ||||
|   ../watch_faces/complication/geomancy_face.c \
 | ||||
|   ../watch_faces/clock/simple_clock_bin_led_face.c \
 | ||||
|   ../watch_faces/complication/menstrual_cycle_face.c \
 | ||||
|   ../watch_faces/complication/flashlight_face.c \
 | ||||
|   ../watch_faces/clock/decimal_time_face.c \
 | ||||
|   ../watch_faces/clock/wyoscan_face.c \
 | ||||
|  | ||||
| @ -94,6 +94,7 @@ | ||||
| #include "geomancy_face.h" | ||||
| #include "dual_timer_face.h" | ||||
| #include "simple_clock_bin_led_face.h" | ||||
| #include "menstrual_cycle_face.h" | ||||
| #include "flashlight_face.h" | ||||
| #include "decimal_time_face.h" | ||||
| #include "wyoscan_face.h" | ||||
|  | ||||
							
								
								
									
										472
									
								
								movement/watch_faces/complication/menstrual_cycle_face.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										472
									
								
								movement/watch_faces/complication/menstrual_cycle_face.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,472 @@ | ||||
| /*
 | ||||
|  * MIT License | ||||
|  * | ||||
|  * Copyright (c) 2023 Joseph Borne Komosa | @jokomo24 | ||||
|  * | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in all | ||||
|  * copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
|  * SOFTWARE. | ||||
|  * | ||||
|  *  | ||||
|  * Menstrual Cycle Face | ||||
|  *  | ||||
|  * Background: | ||||
|  *  | ||||
|  * I discovered the Casio F-91W through my partner, appreciated the retro aesthetic of the watch, | ||||
|  * and got one for myself. Soon afterward I discovered the Sensor Watch project and ordered two boards!  | ||||
|  * I introduced the Sensor Watch to my partner who inquired whether she could track her menstrual cycle. | ||||
|  * So I decided to implement a menstrual cycle watch face that also calculates the peak fertility window | ||||
|  * using The Calendar Method. While this information may be useful when attempting to achieve or avoid  | ||||
|  * pregnancy, it is important to understand that these are rough estimates at best. | ||||
|  *  | ||||
|  * How to use: | ||||
|  *  | ||||
|  * 1. To begin tracking, go to 'Last Period' page and toggle the alarm button to the number of days since  | ||||
|  *    the last, most recent, period and hold the alarm button to enter. This will perform the following actions: | ||||
|  *    - Store the corresponding date as the 'first' period in order to calculate the total_days_tracked. | ||||
|  *    - Turn on the Signal Indicator to signify that tracking has been activated. | ||||
|  *    - Deactivate this page and instead show the ticking animation. | ||||
|  *    - Adjust the days left in the 'Period in <num> Days' page accordingly. | ||||
|  *    - Activate the 'Period Is Here' page and no longer display 'NA'. To prevent accidental user entry, | ||||
|  *      the page will display the ticking animation until ten days have passed since the date of the last  | ||||
|  *      period entered. | ||||
|  *    - Activate the 'Peak Fertility' page to begin showing the estimated window, | ||||
|  *      as well as display the Alarm Indicator, on this page and on the main 'Period in <num> Days' page, | ||||
|  *      whenever the current date falls within the Peak Fertility Window. | ||||
|  *  | ||||
|  * 2. Toggle and enter 'y' in the 'Period Is Here' page on the day of every sequential period afterward.  | ||||
|  *    DO NOT FORGET TO DO SO! | ||||
|  *    - If forgotten, the data will become inaccurate and tracking will need to be reset! -> (FIXME, allow one to enter a 'missed' period using the 'Last Period' page). | ||||
|  *    This will perform the following actions: | ||||
|  *    - Calculate this completed cycle's length and reevaluate the shortest and longest cycle variables. | ||||
|  *    - Increment total_cycles by one. | ||||
|  *    - Recalculate and save the average cycle for 'Average Cycle' page. | ||||
|  */ | ||||
| 
 | ||||
| #include <stdlib.h> | ||||
| #include <string.h> | ||||
| #include "menstrual_cycle_face.h" | ||||
| #include "watch.h" | ||||
| #include "watch_utility.h" | ||||
| 
 | ||||
| #define TYPICAL_AVG_CYC 28 | ||||
| #define SECONDS_PER_DAY 86400 | ||||
| 
 | ||||
| #define MENSTRUAL_CYCLE_FACE_NUM_PAGES (6) | ||||
| enum { | ||||
|     period_in_num_days, | ||||
|     average_cycle, | ||||
|     peak_fertility_window, | ||||
|     period_is_here, | ||||
|     first_period, | ||||
|     reset, | ||||
| } page_titles_e; | ||||
| const char menstrual_cycle_face_titles[MENSTRUAL_CYCLE_FACE_NUM_PAGES][11] = { | ||||
|     "Prin   day",   // Period In <num> Days: Estimated days till the next period occurs
 | ||||
|     "Av  cycle ",   // Average Cycle: The average number of days estimated per cycle
 | ||||
|     "Peak Fert ",   // Peak Fertility Window: The first and last day of month (displayed top & bottom right, respectively, once tracking) for the estimated window of fertility
 | ||||
|     "Prishere  ",   // Period Is Here: Toggle and enter 'y' on the day the actual period occurs to improve Avg and Fert estimations
 | ||||
|     "Last Per  ",   // Last Period: Enter the number of days since the last period to begin tracking from that corresponding date by storing it as the 'first'
 | ||||
|     "    Reset ",   // Reset: Toggle and enter 'y' to reset tracking data
 | ||||
| }; | ||||
| 
 | ||||
| /* Beep function */ | ||||
| static inline void beep(movement_settings_t *settings) { | ||||
|     if (settings->bit.button_should_sound)  | ||||
|         watch_buzzer_play_note(BUZZER_NOTE_E8, 75); | ||||
| } | ||||
| 
 | ||||
| // Calculate the total number of days for which menstrual cycle tracking has been active
 | ||||
| static inline uint32_t total_days_tracked(menstrual_cycle_state_t *state) { | ||||
| 
 | ||||
|     // If tracking has not yet been activated, return 0
 | ||||
|     if (!(state->dates.reg))  | ||||
|         return 0; | ||||
| 
 | ||||
|     // Otherwise, set the start date to the first day of the first tracked cycle
 | ||||
|     watch_date_time date_time_start; | ||||
|     date_time_start.unit.second = 0; | ||||
|     date_time_start.unit.minute = 0; | ||||
|     date_time_start.unit.hour = 0; | ||||
|     date_time_start.unit.day = state->dates.bit.first_day; | ||||
|     date_time_start.unit.month = state->dates.bit.first_month; | ||||
|     date_time_start.unit.year = state->dates.bit.first_year; | ||||
| 
 | ||||
|     // Get the current date and time
 | ||||
|     watch_date_time date_time_now = watch_rtc_get_date_time(); | ||||
| 
 | ||||
|     // Convert the start date and current date to Unix time
 | ||||
|     uint32_t unix_start = watch_utility_date_time_to_unix_time(date_time_start, state->utc_offset); | ||||
|     uint32_t unix_now = watch_utility_date_time_to_unix_time(date_time_now, state->utc_offset); | ||||
| 
 | ||||
|     // Calculate the total number of days and return it
 | ||||
|     return (unix_now - unix_start) / SECONDS_PER_DAY; | ||||
| } | ||||
| 
 | ||||
| // Calculate the number of days until the next menstrual period
 | ||||
| static inline int8_t days_till_period(menstrual_cycle_state_t *state) { | ||||
| 
 | ||||
|     // Calculate the number of days left until the next period based on the average cycle length and the number of cycles tracked
 | ||||
|     int8_t days_left = (state->cycles.bit.average_cycle * (state->cycles.bit.total_cycles + 1)) - total_days_tracked(state); | ||||
| 
 | ||||
|     // If the result is negative, return 0 (i.e., the period is expected to start today or has already started)
 | ||||
|     return (days_left < 0) ? 0 : days_left; | ||||
| } | ||||
| 
 | ||||
| static inline void reset_tracking(menstrual_cycle_state_t *state) { | ||||
| 
 | ||||
|     state->dates.bit.first_day = 0; | ||||
|     state->dates.bit.first_month = 0; | ||||
|     state->dates.bit.first_year = 0; | ||||
| 
 | ||||
|     state->dates.bit.prev_day = 0; | ||||
|     state->dates.bit.prev_month = 0; | ||||
|     state->dates.bit.prev_year = 0; | ||||
| 
 | ||||
|     state->cycles.bit.shortest_cycle = TYPICAL_AVG_CYC; | ||||
|     state->cycles.bit.longest_cycle = TYPICAL_AVG_CYC; | ||||
|     state->cycles.bit.average_cycle = TYPICAL_AVG_CYC; | ||||
|     state->cycles.bit.total_cycles = 0; | ||||
| 
 | ||||
|     state->dates.bit.reserved = 0; | ||||
|     state->cycles.bit.reserved = 0; | ||||
| 
 | ||||
|     watch_store_backup_data(state->dates.reg, state->backup_register_dt); | ||||
|     watch_store_backup_data(state->cycles.reg, state->backup_register_cy); | ||||
| 
 | ||||
|     watch_clear_indicator(WATCH_INDICATOR_SIGNAL); | ||||
| } | ||||
| 
 | ||||
| /*
 | ||||
| Fertility Window based on "The Calendar Method" | ||||
| Source: https://www.womenshealth.gov/pregnancy/you-get-pregnant/trying-conceive
 | ||||
| 
 | ||||
| The Calendar Method has several steps: | ||||
| 
 | ||||
| Step 1: Track the menstrual cycle for 8–12 months. One cycle is from the first day of one  | ||||
|         period until the first day of the next period. The average cycle is 28 days, but  | ||||
|         it may be as short as 24 days or as long as 38 days. | ||||
| Step 2: Subtract 18 from the number of days in the shortest menstrual cycle. | ||||
| Step 3: Subtract 11 from the number of days in the longest menstrual cycle. | ||||
| Step 4: Using a calendar, mark down the start of the next period (using previous instead). Count ahead by the number | ||||
|         of days calculated in step 2. This is when peak fertility begins. Peak fertility ends | ||||
|         at the number of days calculated in step 3. | ||||
| NOTE: Right now, the fertility window face displays its estimated window as soon as tracking is activated, although | ||||
|       it is important to keep in mind that The Calendar Method states that peak accuracy of the window will be  | ||||
|       reached only after at least 8 months of tracking the menstrual cycle (can make it so that it only displays | ||||
|       after total_days_tracked >= 8 months...but the info is interesting and should already be taken with the understanding that, | ||||
|       in general, it is a rough estimation at best). | ||||
| */ | ||||
| typedef enum Fertile_Window {first_day, last_day} fertile_window; | ||||
| // Calculate the predicted starting or ending day of peak fertility
 | ||||
| static inline uint32_t get_day_pk_fert(menstrual_cycle_state_t *state, fertile_window which_day) { | ||||
| 
 | ||||
|     // Get the date of the previous period
 | ||||
|     watch_date_time date_prev_period; | ||||
|     date_prev_period.unit.second = 0; | ||||
|     date_prev_period.unit.minute = 0; | ||||
|     date_prev_period.unit.hour = 0; | ||||
|     date_prev_period.unit.day = state->dates.bit.prev_day; | ||||
|     date_prev_period.unit.month = state->dates.bit.prev_month; | ||||
|     date_prev_period.unit.year = state->dates.bit.prev_year; | ||||
| 
 | ||||
|     // Convert the previous period date to Unix time
 | ||||
|     uint32_t unix_prev_period = watch_utility_date_time_to_unix_time(date_prev_period, state->utc_offset); | ||||
| 
 | ||||
|     // Calculate the Unix time of the predicted peak fertility day based on the length of the shortest/longest cycle
 | ||||
|     uint32_t unix_pk_date; | ||||
|     switch(which_day) { | ||||
|         case first_day: | ||||
|             unix_pk_date = unix_prev_period + ((state->cycles.bit.shortest_cycle - 18) * SECONDS_PER_DAY); | ||||
|             break; | ||||
|         case last_day: | ||||
|             unix_pk_date = unix_prev_period + ((state->cycles.bit.longest_cycle - 11) * SECONDS_PER_DAY); | ||||
|             break; | ||||
|     } | ||||
| 
 | ||||
|     // Convert the Unix time of the predicted peak fertility day to a date/time and return the day of the month
 | ||||
|     return watch_utility_date_time_from_unix_time(unix_pk_date, state->utc_offset).unit.day; | ||||
| } | ||||
| 
 | ||||
| // Determine if today falls within the predicted peak fertility window
 | ||||
| static inline bool inside_fert_window(menstrual_cycle_state_t *state) { | ||||
| 
 | ||||
|     // If tracking has not yet been activated, return false
 | ||||
|     if (!(state->dates.reg))  | ||||
|         return false; | ||||
| 
 | ||||
|     // Get the current date/time
 | ||||
|     watch_date_time date_time_now = watch_rtc_get_date_time(); | ||||
| 
 | ||||
|     // Check if the current day falls between the first and last predicted peak fertility days
 | ||||
|     if (get_day_pk_fert(state, first_day) > get_day_pk_fert(state, last_day)) { // We are crossing over the end of the month
 | ||||
|         if (date_time_now.unit.day >= get_day_pk_fert(state, first_day) || | ||||
|             date_time_now.unit.day <= get_day_pk_fert(state, last_day)) | ||||
|             return true; | ||||
|     } | ||||
|     else if (date_time_now.unit.day >= get_day_pk_fert(state, first_day) && | ||||
|              date_time_now.unit.day <= get_day_pk_fert(state, last_day)) | ||||
|              return true; | ||||
|     // If the current day does not fall within the predicted peak fertility window, return false
 | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| // Update the shortest and longest menstrual cycles based on the previous menstrual cycle
 | ||||
| static inline void update_shortest_longest_cycle(menstrual_cycle_state_t *state) { | ||||
| 
 | ||||
|     // Get the date of the previous menstrual cycle
 | ||||
|     watch_date_time date_prev_period; | ||||
|     date_prev_period.unit.second = 0; | ||||
|     date_prev_period.unit.minute = 0; | ||||
|     date_prev_period.unit.hour = 0; | ||||
|     date_prev_period.unit.day = state->dates.bit.prev_day; | ||||
|     date_prev_period.unit.month = state->dates.bit.prev_month; | ||||
|     date_prev_period.unit.year = state->dates.bit.prev_year; | ||||
| 
 | ||||
|     // Convert the date of the previous menstrual cycle to UNIX time
 | ||||
|     uint32_t unix_prev_period = watch_utility_date_time_to_unix_time(date_prev_period, state->utc_offset); | ||||
| 
 | ||||
|     // Calculate the length of the current menstrual cycle
 | ||||
|     uint8_t cycle_length = total_days_tracked(state) - (unix_prev_period / SECONDS_PER_DAY); | ||||
| 
 | ||||
|     // Update the shortest or longest cycle length if necessary
 | ||||
|     if (cycle_length < state->cycles.bit.shortest_cycle) | ||||
|         state->cycles.bit.shortest_cycle = cycle_length; | ||||
|     else if (cycle_length > state->cycles.bit.longest_cycle) | ||||
|         state->cycles.bit.longest_cycle = cycle_length; | ||||
| } | ||||
| 
 | ||||
| void menstrual_cycle_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { | ||||
|     (void) watch_face_index; | ||||
|     (void) settings; | ||||
|      | ||||
|     if (*context_ptr == NULL) { | ||||
|         *context_ptr = malloc(sizeof(menstrual_cycle_state_t)); | ||||
|         memset(*context_ptr, 0, sizeof(menstrual_cycle_state_t)); | ||||
|         menstrual_cycle_state_t *state = ((menstrual_cycle_state_t *)*context_ptr); | ||||
| 
 | ||||
|         state->dates.bit.first_day = 0; | ||||
|         state->dates.bit.first_month = 0; | ||||
|         state->dates.bit.first_year = 0; | ||||
| 
 | ||||
|         state->dates.bit.prev_day = 0; | ||||
|         state->dates.bit.prev_month = 0; | ||||
|         state->dates.bit.prev_year = 0; | ||||
| 
 | ||||
|         state->cycles.bit.shortest_cycle = TYPICAL_AVG_CYC; | ||||
|         state->cycles.bit.longest_cycle = TYPICAL_AVG_CYC; | ||||
|         state->cycles.bit.average_cycle = TYPICAL_AVG_CYC; | ||||
|         state->cycles.bit.total_cycles = 0; | ||||
| 
 | ||||
|         state->dates.bit.reserved = 0; | ||||
|         state->cycles.bit.reserved = 0; | ||||
| 
 | ||||
|         state->backup_register_dt = 0; | ||||
|         state->backup_register_cy = 0; | ||||
|     } | ||||
| 
 | ||||
|     menstrual_cycle_state_t *state = ((menstrual_cycle_state_t *)*context_ptr); | ||||
|     if (!(state->backup_register_dt && state->backup_register_cy)) { | ||||
|         state->backup_register_dt = movement_claim_backup_register(); | ||||
|         state->backup_register_cy = movement_claim_backup_register(); | ||||
| 
 | ||||
|         if (state->backup_register_dt && state->backup_register_cy) { | ||||
|             watch_store_backup_data(state->dates.reg, state->backup_register_dt); | ||||
|             watch_store_backup_data(state->cycles.reg, state->backup_register_cy); | ||||
|         } | ||||
|     } | ||||
|     else { | ||||
|         state->dates.reg = watch_get_backup_data(state->backup_register_dt); | ||||
|         state->cycles.reg = watch_get_backup_data(state->backup_register_cy); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void menstrual_cycle_face_activate(movement_settings_t *settings, void *context) { | ||||
|     (void) settings; | ||||
|     menstrual_cycle_state_t *state = (menstrual_cycle_state_t *)context; | ||||
|     state->period_today = 0; | ||||
|     state->current_page = 0; | ||||
|     state->reset_tracking = 0; | ||||
|     state->utc_offset = movement_timezone_offsets[settings->bit.time_zone] * 60; | ||||
|     movement_request_tick_frequency(4); // we need to manually blink some pixels
 | ||||
| } | ||||
| 
 | ||||
| bool menstrual_cycle_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { | ||||
|     menstrual_cycle_state_t *state = (menstrual_cycle_state_t *)context; | ||||
|     watch_date_time date_period; | ||||
|     uint8_t current_page = state->current_page; | ||||
|     uint8_t first_day_fert; | ||||
|     uint8_t last_day_fert; | ||||
|     uint32_t unix_now; | ||||
|     uint32_t unix_prev_period; | ||||
|     switch (event.event_type) { | ||||
|         case EVENT_TICK: | ||||
|         case EVENT_ACTIVATE: | ||||
|             // Do nothing; handled below.
 | ||||
|             break; | ||||
|         case EVENT_MODE_BUTTON_UP: | ||||
|             movement_move_to_next_face(); | ||||
|             return false; | ||||
|         case EVENT_LIGHT_BUTTON_DOWN: | ||||
|             current_page = (current_page + 1) % MENSTRUAL_CYCLE_FACE_NUM_PAGES; | ||||
|             state->current_page = current_page; | ||||
|             state->days_prev_period = 0; | ||||
|             watch_clear_indicator(WATCH_INDICATOR_BELL); | ||||
|             if (watch_tick_animation_is_running()) | ||||
|                 watch_stop_tick_animation(); | ||||
|             break; | ||||
|         case EVENT_ALARM_LONG_PRESS: | ||||
|             switch (current_page) { | ||||
|                 case period_in_num_days: | ||||
|                     break; | ||||
|                 case average_cycle: | ||||
|                     break; | ||||
|                 case peak_fertility_window: | ||||
|                     break; | ||||
|                 case period_is_here: | ||||
|                     if (state->period_today && total_days_tracked(state)) { | ||||
|                         // Calculate before updating date of last period
 | ||||
|                         update_shortest_longest_cycle(state); | ||||
|                         // Update the date of last period after calulating the, now previous, cycle length
 | ||||
|                         date_period = watch_rtc_get_date_time(); | ||||
|                         state->dates.bit.prev_day = date_period.unit.day; | ||||
|                         state->dates.bit.prev_month = date_period.unit.month; | ||||
|                         state->dates.bit.prev_year = date_period.unit.year; | ||||
|                         // Calculate new cycle average
 | ||||
|                         state->cycles.bit.total_cycles += 1; | ||||
|                         state->cycles.bit.average_cycle = total_days_tracked(state) / state->cycles.bit.total_cycles; | ||||
|                         // Store the new data
 | ||||
|                         watch_store_backup_data(state->dates.reg, state->backup_register_dt); | ||||
|                         watch_store_backup_data(state->cycles.reg, state->backup_register_cy); | ||||
|                         state->period_today = !(state->period_today); | ||||
|                         beep(settings); | ||||
|                     } | ||||
|                     break; | ||||
|                 case first_period: | ||||
|                     // If tracking has not yet been activated
 | ||||
|                     if (!(state->dates.reg)) { | ||||
|                         unix_now = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), state->utc_offset); | ||||
|                         unix_prev_period = unix_now - (state->days_prev_period * SECONDS_PER_DAY); | ||||
|                         date_period = watch_utility_date_time_from_unix_time(unix_prev_period, state->utc_offset); | ||||
|                         state->dates.bit.first_day = date_period.unit.day; | ||||
|                         state->dates.bit.first_month = date_period.unit.month; | ||||
|                         state->dates.bit.first_year = date_period.unit.year; | ||||
|                         state->dates.bit.prev_day = date_period.unit.day; | ||||
|                         state->dates.bit.prev_month = date_period.unit.month; | ||||
|                         state->dates.bit.prev_year = date_period.unit.year; | ||||
|                         watch_store_backup_data(state->dates.reg, state->backup_register_dt); | ||||
|                         beep(settings); | ||||
|                     } | ||||
|                     break; | ||||
|                 case reset: | ||||
|                     if (state->reset_tracking) { | ||||
|                         reset_tracking(state); | ||||
|                         state->reset_tracking = !(state->reset_tracking); | ||||
|                         beep(settings); | ||||
|                     } | ||||
|                     break; | ||||
|             } | ||||
|             break; | ||||
|         case EVENT_ALARM_BUTTON_UP: | ||||
|             switch (current_page) { | ||||
|                 case period_in_num_days: | ||||
|                     break; | ||||
|                 case average_cycle: | ||||
|                     break; | ||||
|                 case peak_fertility_window: | ||||
|                     break; | ||||
|                 case period_is_here: | ||||
|                     if (total_days_tracked(state)) | ||||
|                         state->period_today = !(state->period_today); | ||||
|                     break; | ||||
|                 case first_period: | ||||
|                     if (!(state->dates.reg))  | ||||
|                         state->days_prev_period = (state->days_prev_period > 99) ? 0 : state->days_prev_period + 1; // Cycle through pages to quickly reset to 0
 | ||||
|                     break; | ||||
|                 case reset: | ||||
|                     state->reset_tracking = !(state->reset_tracking); | ||||
|                     break; | ||||
|             } | ||||
|             break; | ||||
|         case EVENT_TIMEOUT: | ||||
|             movement_move_to_face(0); | ||||
|             break; | ||||
|         default: | ||||
|             return movement_default_loop_handler(event, settings); | ||||
|     } | ||||
| 
 | ||||
|     watch_display_string((char *)menstrual_cycle_face_titles[current_page], 0); | ||||
|     if (state->dates.reg) | ||||
|         watch_set_indicator(WATCH_INDICATOR_SIGNAL); // signal that we are now in a tracking state
 | ||||
| 
 | ||||
|     char buf[13]; | ||||
|     switch (current_page) { | ||||
|         case period_in_num_days: | ||||
|             sprintf(buf, "%2d", days_till_period(state)); | ||||
|             if (inside_fert_window(state)) | ||||
|                 watch_set_indicator(WATCH_INDICATOR_BELL); | ||||
|             watch_display_string(buf, 4); | ||||
|             break; | ||||
|         case average_cycle: | ||||
|             sprintf(buf, "%2d", state->cycles.bit.average_cycle); | ||||
|             watch_display_string(buf, 2); | ||||
|             break; | ||||
|         case peak_fertility_window: | ||||
|             if (event.subsecond % 5 && state->dates.reg) { // blink active for 3 quarter-seconds
 | ||||
|                 first_day_fert = get_day_pk_fert(state, first_day); | ||||
|                 last_day_fert = get_day_pk_fert(state, last_day); | ||||
|                 sprintf(buf, "Fr%2d To %2d", first_day_fert, last_day_fert); // From: first day | To: last day
 | ||||
|                 if (inside_fert_window(state)) | ||||
|                     watch_set_indicator(WATCH_INDICATOR_BELL); | ||||
|                 watch_display_string(buf, 0); | ||||
|             } | ||||
|             break; | ||||
|         case period_is_here: | ||||
|             if (event.subsecond % 5) { // blink active for 3 quarter-seconds
 | ||||
|                 if (!(state->dates.reg)) | ||||
|                     watch_display_string("NA", 8); // Not Applicable: Do not allow period entry until tracking is activated...
 | ||||
|                 else if (state->period_today)  | ||||
|                     watch_display_string("y", 9); | ||||
|                 else  | ||||
|                     watch_display_string("n", 9); | ||||
|             } | ||||
|             break; | ||||
|         case first_period: | ||||
|             if (state->dates.reg) { | ||||
|                 if (!watch_tick_animation_is_running()) | ||||
|                     watch_start_tick_animation(500); // Tracking activated
 | ||||
|             } | ||||
|             else if (event.subsecond % 5) { // blink active for 3 quarter-seconds
 | ||||
|                 sprintf(buf, "%2d", state->days_prev_period); | ||||
|                 watch_display_string(buf, 8); | ||||
|             } | ||||
|             break; | ||||
|         case reset: | ||||
|             // blink active for 3 quarter-seconds
 | ||||
|             if (event.subsecond % 5 && state->reset_tracking) | ||||
|                 watch_display_string("y", 9); | ||||
|             else if (event.subsecond % 5) | ||||
|                 watch_display_string("n", 9); | ||||
|             break; | ||||
|     } | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| void menstrual_cycle_face_resign(movement_settings_t *settings, void *context) { | ||||
|     (void) settings; | ||||
|     (void) context; | ||||
| } | ||||
							
								
								
									
										80
									
								
								movement/watch_faces/complication/menstrual_cycle_face.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								movement/watch_faces/complication/menstrual_cycle_face.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | ||||
| /*
 | ||||
|  * MIT License | ||||
|  * | ||||
|  * Copyright (c) 2023 Joseph Borne Komosa | @jokomo24 | ||||
|  * | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in all | ||||
|  * copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
|  * SOFTWARE. | ||||
|  */ | ||||
| 
 | ||||
| #ifndef MENSTRUAL_CYCLE_FACE_H_ | ||||
| #define MENSTRUAL_CYCLE_FACE_H_ | ||||
| 
 | ||||
| #include "movement.h" | ||||
| 
 | ||||
| typedef struct { | ||||
|     // Store the date of the 'first' and the total cycles since to calulate and store the average menstrual cycle.
 | ||||
|     // Store the date of the previous, most recent, period to calculate the cycle length.
 | ||||
|     // Store the shortest and longest cycle to calculate the fertility window for The Calender Method.
 | ||||
|     // NOTE: Not thrilled about using two registers, but could not find a way to perform The Calender Method
 | ||||
|     //       without requiring both the 'first' and 'prev' dates.
 | ||||
|     union { | ||||
|         struct { | ||||
|             uint8_t first_day : 5; | ||||
|             uint8_t first_month : 4; | ||||
|             uint8_t first_year : 6; // 0-63 (representing 2020-2083)
 | ||||
|             uint8_t prev_day : 5; | ||||
|             uint8_t prev_month : 4; | ||||
|             uint8_t prev_year : 6; // 0-63 (representing 2020-2083)
 | ||||
|             uint8_t reserved : 2; // left over bit space
 | ||||
|         } bit; | ||||
|         uint32_t reg; // Tracking's been activated if > 0
 | ||||
|     } dates; | ||||
|     union { | ||||
|         struct { | ||||
|             uint8_t shortest_cycle : 6; // For step 2 of The Calender Method 
 | ||||
|             uint8_t longest_cycle : 6; // For step 3 of The Calender Method 
 | ||||
|             uint8_t average_cycle : 6; // The average menstrual cycle lasts 28 days, but normal cycles can vary from 21 to 35 days
 | ||||
|             uint16_t total_cycles : 11; // The total cycles (periods) entered since the start of tracking
 | ||||
|             uint8_t reserved : 3; // left over bit space
 | ||||
|         } bit;  | ||||
|         uint32_t reg; | ||||
|     } cycles; | ||||
|     uint8_t backup_register_dt; | ||||
|     uint8_t backup_register_cy; | ||||
|     uint8_t current_page; | ||||
|     uint8_t days_prev_period; | ||||
|     int32_t utc_offset; | ||||
|     bool period_today; | ||||
|     bool reset_tracking; | ||||
| } menstrual_cycle_state_t; | ||||
| 
 | ||||
| void menstrual_cycle_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); | ||||
| void menstrual_cycle_face_activate(movement_settings_t *settings, void *context); | ||||
| bool menstrual_cycle_face_loop(movement_event_t event, movement_settings_t *settings, void *context); | ||||
| void menstrual_cycle_face_resign(movement_settings_t *settings, void *context); | ||||
| 
 | ||||
| #define menstrual_cycle_face ((const watch_face_t){ \ | ||||
|     menstrual_cycle_face_setup, \ | ||||
|     menstrual_cycle_face_activate, \ | ||||
|     menstrual_cycle_face_loop, \ | ||||
|     menstrual_cycle_face_resign, \ | ||||
|     NULL, \ | ||||
| }) | ||||
| 
 | ||||
| #endif // MENSTRUAL_CYCLE_FACE_H_
 | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user