From 0f9bb0bd37c8183cf8eb0f3385c831503d1f76bf Mon Sep 17 00:00:00 2001 From: Konrad Rieck Date: Fri, 11 Aug 2023 22:26:34 +0200 Subject: [PATCH 01/45] Fixed incorrect conversion from UNIX timestamp to watch_date_time. --- watch-library/shared/watch/watch_utility.c | 80 ++++++++++++++++++++-- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/watch-library/shared/watch/watch_utility.c b/watch-library/shared/watch/watch_utility.c index 9e524762..64b3bb79 100644 --- a/watch-library/shared/watch/watch_utility.c +++ b/watch-library/shared/watch/watch_utility.c @@ -102,13 +102,81 @@ uint16_t watch_utility_days_since_new_year(uint16_t year, uint8_t month, uint8_t return (is_leap(year) && (month > 2) ? 1 : 0) + DAYS_SO_FAR[month - 1] + day; } -uint32_t watch_utility_convert_to_unix_time(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second, uint32_t utc_offset) { - uint32_t year_adj = year + 4800; - uint32_t leap_days = 1 + (year_adj / 4) - (year_adj / 100) + (year_adj / 400); - uint32_t days = 365 * year_adj + leap_days + watch_utility_days_since_new_year(year, month, day) - 1; - days -= 2472692; /* Adjust to Unix epoch. */ +// Function taken from `src/time/__year_to_secs.c` of musl libc +// https://musl.libc.org +static uint32_t __year_to_secs(uint32_t year, int *is_leap) +{ + if (year-2ULL <= 136) { + int y = year; + int leaps = (y-68)>>2; + if (!((y-68)&3)) { + leaps--; + if (is_leap) *is_leap = 1; + } else if (is_leap) *is_leap = 0; + return 31536000*(y-70) + 86400*leaps; + } - uint32_t timestamp = days * 86400; + int cycles, centuries, leaps, rem; + + if (!is_leap) is_leap = &(int){0}; + cycles = (year-100) / 400; + rem = (year-100) % 400; + if (rem < 0) { + cycles--; + rem += 400; + } + if (!rem) { + *is_leap = 1; + centuries = 0; + leaps = 0; + } else { + if (rem >= 200) { + if (rem >= 300) centuries = 3, rem -= 300; + else centuries = 2, rem -= 200; + } else { + if (rem >= 100) centuries = 1, rem -= 100; + else centuries = 0; + } + if (!rem) { + *is_leap = 0; + leaps = 0; + } else { + leaps = rem / 4U; + rem %= 4U; + *is_leap = !rem; + } + } + + leaps += 97*cycles + 24*centuries - *is_leap; + + return (year-100) * 31536000LL + leaps * 86400LL + 946684800 + 86400; +} + +// Function taken from `src/time/__month_to_secs.c` of musl libc +// https://musl.libc.org +static int __month_to_secs(int month, int is_leap) +{ + static const int secs_through_month[] = { + 0, 31*86400, 59*86400, 90*86400, + 120*86400, 151*86400, 181*86400, 212*86400, + 243*86400, 273*86400, 304*86400, 334*86400 }; + int t = secs_through_month[month]; + if (is_leap && month >= 2) t+=86400; + return t; +} + +// Function adapted from `src/time/__tm_to_secs.c` of musl libc +// https://musl.libc.org +uint32_t watch_utility_convert_to_unix_time(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second, uint32_t utc_offset) { + int is_leap; + + // POSIX tm struct starts year at 1900 and month at 0 + // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/time.h.html + uint32_t timestamp = __year_to_secs(year - 1900, &is_leap); + timestamp += __month_to_secs(month - 1, is_leap); + + // Regular conversion from musl libc + timestamp += (day - 1) * 86400; timestamp += hour * 3600; timestamp += minute * 60; timestamp += second; From a5abf7ff7a105ebfe31b804d1e951b91a6c0a050 Mon Sep 17 00:00:00 2001 From: Navaneeth Bhardwaj Date: Sun, 3 Sep 2023 00:17:45 +0530 Subject: [PATCH 02/45] Print memory percentages this gives better idea of memories used. --- make.mk | 1 + 1 file changed, 1 insertion(+) diff --git a/make.mk b/make.mk index bf61708c..2870c4f3 100644 --- a/make.mk +++ b/make.mk @@ -62,6 +62,7 @@ CFLAGS += -MD -MP -MT $(BUILD)/$(*F).o -MF $(BUILD)/$(@F).d LDFLAGS += -mcpu=cortex-m0plus -mthumb LDFLAGS += -Wl,--gc-sections LDFLAGS += -Wl,--script=$(TOP)/watch-library/hardware/linker/saml22j18.ld +LDFLAGS += -Wl,--print-memory-usage LIBS += -lm From 9e88f37ced6a21740803f804c6528fc472ca3dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Waag=C3=B8?= Date: Sun, 19 Mar 2023 21:46:21 +0100 Subject: [PATCH 03/45] new face: Tuning tones Add a new face that plays out tones that can be used as a reference when tuning musical instruments. --- movement/make/Makefile | 1 + movement/movement_faces.h | 1 + .../complication/tuning_tones_face.c | 140 ++++++++++++++++++ .../complication/tuning_tones_face.h | 57 +++++++ 4 files changed, 199 insertions(+) create mode 100644 movement/watch_faces/complication/tuning_tones_face.c create mode 100644 movement/watch_faces/complication/tuning_tones_face.h diff --git a/movement/make/Makefile b/movement/make/Makefile index 625c7729..06e1725d 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -118,6 +118,7 @@ SRCS += \ ../watch_faces/complication/flashlight_face.c \ ../watch_faces/clock/decimal_time_face.c \ ../watch_faces/clock/wyoscan_face.c \ + ../watch_faces/complication/tuning_tones_face.c \ # New watch faces go above this line. # Leave this line at the bottom of the file; it has all the targets for making your project. diff --git a/movement/movement_faces.h b/movement/movement_faces.h index ff34c063..23c2613f 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -95,6 +95,7 @@ #include "flashlight_face.h" #include "decimal_time_face.h" #include "wyoscan_face.h" +#include "tuning_tones_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/complication/tuning_tones_face.c b/movement/watch_faces/complication/tuning_tones_face.c new file mode 100644 index 00000000..a139427a --- /dev/null +++ b/movement/watch_faces/complication/tuning_tones_face.c @@ -0,0 +1,140 @@ +/* + * MIT License + * + * Copyright (c) 2023 Per Waagø + * + * 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. + */ + +#include +#include +#include "tuning_tones_face.h" + +/* + + This face plays a tone that can be used as a reference when tuning + musical instrument. + + - The alarm button (short press) starts and stops the tone + - The light button (short press) changes which note is played. The name + of the note is shown in the display. + +*/ + +typedef struct Note { + BuzzerNote note; + char * name; +} Note; + +static Note notes[] = { + { .note = BUZZER_NOTE_C5, .name = "C " }, + { .note = BUZZER_NOTE_C5SHARP_D5FLAT, .name = "Db" }, + { .note = BUZZER_NOTE_D5, .name = "D " }, + { .note = BUZZER_NOTE_D5SHARP_E5FLAT, .name = "Eb" }, + { .note = BUZZER_NOTE_E5, .name = "E " }, + { .note = BUZZER_NOTE_F5, .name = "F " }, + { .note = BUZZER_NOTE_F5SHARP_G5FLAT, .name = "Gb" }, + { .note = BUZZER_NOTE_G5, .name = "G " }, + { .note = BUZZER_NOTE_G5SHARP_A5FLAT, .name = "Ab" }, + { .note = BUZZER_NOTE_A5, .name = "A " }, + { .note = BUZZER_NOTE_A5SHARP_B5FLAT, .name = "Bb" }, + { .note = BUZZER_NOTE_B5, .name = "B " }, +}; + +static size_t note_count = sizeof notes / sizeof *notes; + +static void draw(tuning_tones_state_t *state) +{ + watch_display_string(notes[state->note_ind].name, 8); +} + +void tuning_tones_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (void) watch_face_index; + if (*context_ptr == NULL) { + tuning_tones_state_t *state = malloc(sizeof *state); + memset(state, 0, sizeof *state); + state->note_ind = 9; + *context_ptr = state; + } +} + +void tuning_tones_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + +static void update_buzzer(const tuning_tones_state_t *state) +{ + if (state->playing) { + watch_set_buzzer_off(); + watch_set_buzzer_period(NotePeriods[notes[state->note_ind].note]); + watch_set_buzzer_on(); + } +} + +bool tuning_tones_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + tuning_tones_state_t *state = (tuning_tones_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + draw(state); + break; + case EVENT_TICK: + break; + case EVENT_LIGHT_BUTTON_DOWN: + state->note_ind++; + if (state->note_ind == note_count) { + state->note_ind = 0; + } + update_buzzer(state); + draw(state); + break; + case EVENT_LIGHT_BUTTON_UP: + break; + case EVENT_ALARM_BUTTON_DOWN: + state->playing = !state->playing; + if (!state->playing) { + watch_set_buzzer_off(); + } else { + update_buzzer(state); + } + case EVENT_ALARM_BUTTON_UP: + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + case EVENT_LOW_ENERGY_UPDATE: + break; + default: + return movement_default_loop_handler(event, settings); + } + + return !state->playing; +} + +void tuning_tones_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + tuning_tones_state_t *state = (tuning_tones_state_t *)context; + + if (state->playing) { + state->playing = false; + watch_set_buzzer_off(); + } +} diff --git a/movement/watch_faces/complication/tuning_tones_face.h b/movement/watch_faces/complication/tuning_tones_face.h new file mode 100644 index 00000000..d6e3495e --- /dev/null +++ b/movement/watch_faces/complication/tuning_tones_face.h @@ -0,0 +1,57 @@ +/* + * MIT License + * + * Copyright (c) 2023 Per Waagø + * + * 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 TUNING_TONES_FACE_H_ +#define TUNING_TONES_FACE_H_ + +#include "movement.h" + +/* + * A DESCRIPTION OF YOUR WATCH FACE + * + * and a description of how use it + * + */ + +typedef struct { + // Anything you need to keep track of, put it here! + bool playing; + size_t note_ind; +} tuning_tones_state_t; + +void tuning_tones_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void tuning_tones_face_activate(movement_settings_t *settings, void *context); +bool tuning_tones_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void tuning_tones_face_resign(movement_settings_t *settings, void *context); + +#define tuning_tones_face ((const watch_face_t){ \ + tuning_tones_face_setup, \ + tuning_tones_face_activate, \ + tuning_tones_face_loop, \ + tuning_tones_face_resign, \ + NULL, \ +}) + +#endif // TUNING_TONES_FACE_H_ + From ebfeb1f21a290f91dc21b28a9eb707ebbaf5615c Mon Sep 17 00:00:00 2001 From: Hugo Chargois Date: Sat, 9 Sep 2023 02:22:08 +0200 Subject: [PATCH 04/45] Turn on the funky segment of pos 0 for char '@' --- watch-library/shared/watch/watch_private_display.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-library/shared/watch/watch_private_display.c b/watch-library/shared/watch/watch_private_display.c index 245b20ed..c12957d9 100644 --- a/watch-library/shared/watch/watch_private_display.c +++ b/watch-library/shared/watch/watch_private_display.c @@ -93,7 +93,7 @@ void watch_display_character(uint8_t character, uint8_t position) { } if (character == 'T' && position == 1) watch_set_pixel(1, 12); // add descender - else if (position == 0 && (character == 'B' || character == 'D')) watch_set_pixel(0, 15); // add funky ninth segment + else if (position == 0 && (character == 'B' || character == 'D' || character == '@')) watch_set_pixel(0, 15); // add funky ninth segment else if (position == 1 && (character == 'B' || character == 'D' || character == '@')) watch_set_pixel(0, 12); // add funky ninth segment } From 2e364f4ef9c27424d909480a2150c24f6e649a02 Mon Sep 17 00:00:00 2001 From: Hugo Chargois Date: Sat, 16 Sep 2023 01:39:52 +0200 Subject: [PATCH 05/45] Add a volume slider in the simulator --- watch-library/simulator/shell.html | 48 ++++++++++++++++++-- watch-library/simulator/watch/watch_buzzer.c | 2 +- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/watch-library/simulator/shell.html b/watch-library/simulator/shell.html index 335b9534..c1162f7d 100644 --- a/watch-library/simulator/shell.html +++ b/watch-library/simulator/shell.html @@ -882,10 +882,22 @@ -
- - - Original F-91W SVG is © 2020 Alexis Philip,
used here under the terms of the MIT license.
+ + + + + + +
+ + + + Original F-91W SVG is © 2020 Alexis Philip,
used here under the terms of the MIT license. +
+ +
@@ -981,6 +993,34 @@ ); } toggleSkin(); + + // emulator runs on localhost:8000 which could very well be used by other + // things, so we'll scope our localStorage keys with a prefix + const localStoragePrefix = "sensorwatch_"; + function setLocalPref(key, val) { + localStorage.setItem(localStoragePrefix+key, val); + } + function getLocalPref(key, dfault) { + let pref = localStorage.getItem(localStoragePrefix+key); + if (pref === null) return dfault; + return pref; + } + + volumeGain = 0.1; + function setVolume(vol) { + setLocalPref("volume", vol); + volumeGain = Math.pow(100, (vol / 100) - 1) - 0.01; + } + + function loadPrefs() { + let vol = +getLocalPref("volume", "50"); + if (isNaN(vol) || vol < 0 || vol > 100) { + vol = 50; + } + document.getElementById("volume").value = vol; + setVolume(vol); + } + loadPrefs(); {{{ SCRIPT }}} diff --git a/watch-library/simulator/watch/watch_buzzer.c b/watch-library/simulator/watch/watch_buzzer.c index 68d9a139..211235df 100644 --- a/watch-library/simulator/watch/watch_buzzer.c +++ b/watch-library/simulator/watch/watch_buzzer.c @@ -152,7 +152,7 @@ void watch_set_buzzer_on(void) { } audioContext._oscillator.frequency.value = 1e6/$0; - audioContext._gain.gain.value = 1; + audioContext._gain.gain.value = volumeGain; }, buzzer_period); } From baadb0b43f0fcd5df09c511142f8a320df34f643 Mon Sep 17 00:00:00 2001 From: Hugo Chargois Date: Sat, 16 Sep 2023 02:39:00 +0200 Subject: [PATCH 06/45] Save the selected skin of the simulator in local storage --- watch-library/simulator/shell.html | 32 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/watch-library/simulator/shell.html b/watch-library/simulator/shell.html index c1162f7d..583829cc 100644 --- a/watch-library/simulator/shell.html +++ b/watch-library/simulator/shell.html @@ -885,8 +885,8 @@
- - + + Original F-91W SVG is © 2020 Alexis Philip,
used here under the terms of the MIT license. @@ -979,20 +979,17 @@ } } - function toggleSkin() { - var isBlack = document.getElementById('f91w').checked; - Array.from(document.getElementsByClassName("f91w")).forEach( - function(element, index, array) { - element.setAttribute('style', 'display:' + (isBlack ? 'inline':'none') + ';'); + const validSkins = ["f91w", "a158wea9"]; + function setSkin(chosenSkin) { + setLocalPref("skin", chosenSkin); + validSkins.forEach(function(skin) { + Array.from(document.getElementsByClassName(skin)).forEach( + function(element) { + element.setAttribute('style', 'display:' + (skin == chosenSkin ? 'inline':'none') + ';'); } - ); - Array.from(document.getElementsByClassName("a158wea9")).forEach( - function(element, index, array) { - element.setAttribute('style', 'display:' + (isBlack ? 'none':'inline') + ';'); - } - ); + ); + }); } - toggleSkin(); // emulator runs on localhost:8000 which could very well be used by other // things, so we'll scope our localStorage keys with a prefix @@ -1019,6 +1016,13 @@ } document.getElementById("volume").value = vol; setVolume(vol); + + let skin = getLocalPref("skin", "f91w"); + if (!validSkins.includes(skin)) { + skin = "f91w"; + } + document.getElementById(skin).checked = true; + setSkin(skin); } loadPrefs(); From c28ba6ef0bfc8f76c156b92b5c0acfdf1df13fcf Mon Sep 17 00:00:00 2001 From: Wesley Aptekar-Cassels Date: Thu, 28 Sep 2023 18:29:32 -0400 Subject: [PATCH 07/45] Don't allow building without setting board color. Fixes: #288 --- make.mk | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/make.mk b/make.mk index a83108bd..31189c8f 100644 --- a/make.mk +++ b/make.mk @@ -207,6 +207,10 @@ ifeq ($(LED), BLUE) CFLAGS += -DWATCH_IS_BLUE_BOARD endif +ifndef COLOR +$(error Set the COLOR variable to RED, BLUE, or GREEN depending on what board you have.) +endif + ifeq ($(COLOR), BLUE) CFLAGS += -DWATCH_IS_BLUE_BOARD endif From 7d353bba1c171e7885e69e88f314ca0dbe521863 Mon Sep 17 00:00:00 2001 From: Wesley Aptekar-Cassels Date: Thu, 5 Oct 2023 12:42:43 -0400 Subject: [PATCH 08/45] Set default board color for GH workflow. I've chosen blue arbitrarily. --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b150afb1..6b4fc793 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,9 @@ on: branches-ignore: - gh-pages +env: + COLOR: BLUE + jobs: build: container: From ad846f50602d3824097e9fbbbac4dcd88d9a8a3f Mon Sep 17 00:00:00 2001 From: LtKeks Date: Sun, 15 Oct 2023 17:35:36 +0200 Subject: [PATCH 09/45] Update timer_face.c Corrects the data type of the standard values in order to be able to configure seconds as well. --- movement/watch_faces/complication/timer_face.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/movement/watch_faces/complication/timer_face.c b/movement/watch_faces/complication/timer_face.c index 70f250a5..8bf7cf99 100644 --- a/movement/watch_faces/complication/timer_face.c +++ b/movement/watch_faces/complication/timer_face.c @@ -30,7 +30,7 @@ #include "watch.h" #include "watch_utility.h" -static const uint16_t _default_timer_values[] = {0x200, 0x500, 0xA00, 0x1400, 0x2D02}; // default timers: 2 min, 5 min, 10 min, 20 min, 2 h 45 min +static const uint32_t _default_timer_values[] = {0x000200, 0x000500, 0x000A00, 0x001400, 0x002D02}; // default timers: 2 min, 5 min, 10 min, 20 min, 2 h 45 min // sound sequence for a single beeping sequence static const int8_t _sound_seq_beep[] = {BUZZER_NOTE_C8, 3, BUZZER_NOTE_REST, 3, -2, 2, BUZZER_NOTE_C8, 5, BUZZER_NOTE_REST, 25, 0}; @@ -199,7 +199,7 @@ void timer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, v timer_state_t *state = (timer_state_t *)*context_ptr; memset(*context_ptr, 0, sizeof(timer_state_t)); state->watch_face_index = watch_face_index; - for (uint8_t i = 0; i < sizeof(_default_timer_values) / sizeof(uint16_t); i++) { + for (uint8_t i = 0; i < sizeof(_default_timer_values) / sizeof(uint32_t); i++) { state->timers[i].value = _default_timer_values[i]; } } From cde6ae35989cd0038ea0973bcc0074b700c796b6 Mon Sep 17 00:00:00 2001 From: Ekaitz Zarraga Date: Wed, 18 Oct 2023 23:29:25 +0200 Subject: [PATCH 10/45] Add Couch-To-5k training face --- movement/make/Makefile | 1 + movement/movement_faces.h | 1 + .../watch_faces/complication/couchTo5k_face.c | 264 ++++++++++++++++++ .../watch_faces/complication/couchTo5k_face.h | 87 ++++++ 4 files changed, 353 insertions(+) create mode 100644 movement/watch_faces/complication/couchTo5k_face.c create mode 100644 movement/watch_faces/complication/couchTo5k_face.h diff --git a/movement/make/Makefile b/movement/make/Makefile index 625c7729..646e3cb8 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -118,6 +118,7 @@ SRCS += \ ../watch_faces/complication/flashlight_face.c \ ../watch_faces/clock/decimal_time_face.c \ ../watch_faces/clock/wyoscan_face.c \ + ../watch_faces/complication/couchTo5k_face.c \ # New watch faces go above this line. # Leave this line at the bottom of the file; it has all the targets for making your project. diff --git a/movement/movement_faces.h b/movement/movement_faces.h index ff34c063..4a98fcf2 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -95,6 +95,7 @@ #include "flashlight_face.h" #include "decimal_time_face.h" #include "wyoscan_face.h" +#include "couchTo5k_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/complication/couchTo5k_face.c b/movement/watch_faces/complication/couchTo5k_face.c new file mode 100644 index 00000000..ca63b76f --- /dev/null +++ b/movement/watch_faces/complication/couchTo5k_face.c @@ -0,0 +1,264 @@ +/* + * MIT License + * + * Copyright (c) 2023 Ekaitz Zarraga + * + * 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. + */ + +#include +#include +#include "couchTo5k_face.h" + +// They go: Warmup, Run, Walk, Run, Walk, Run, Walk ... , End (0) +// Time is defined in seconds +// Maybe do /10 to reduce memory usage? +// (i don't want to use floats) + +// uint16_t C25K_WEEK_TEST[] = {10, 10, 10, 0}; +uint16_t C25K_WEEK_1[] = {300, 60, 90, 60, 90, 60, 90, 60, 90, 60, 90, 60, + 90, 60, 90, 60, 90, 0}; +uint16_t C25K_WEEK_2[] = {300, 90, 120, 90, 120, 90, 120, 90, 120, 90, 120, + 90, 120, 0}; +uint16_t C25K_WEEK_3[] = {300, 90, 90, 180, 180, 90, 90, 180, 180, 0}; +uint16_t C25K_WEEK_4[] = {300, 180, 90, 300, 150, 180, 90, 300, 0}; +uint16_t C25K_WEEK_5_1[] = {300, 300, 180, 300, 180, 300, 0 }; +uint16_t C25K_WEEK_5_2[] = {300, 480, 300, 480 , 0}; +uint16_t C25K_WEEK_5_3[] = {300, 1200, 0}; +uint16_t C25K_WEEK_6_1[] = {300, 300, 180, 480, 180, 300, 0 }; +uint16_t C25K_WEEK_6_2[] = {300, 600, 180, 600 , 0}; +uint16_t C25K_WEEK_6_3[] = {300, 1500, 0}; +uint16_t C25K_WEEK_7[] = {300, 1500, 0}; +uint16_t C25K_WEEK_8[] = {300, 1680, 0}; +uint16_t C25K_WEEK_9[] = {300, 1800, 0}; + + +#define C25K_SESSIONS_LENGTH 3*9 +uint16_t *C25K_SESSIONS[C25K_SESSIONS_LENGTH]; + +static inline bool _finished(couchTo5k_state_t *state){ + return state->exercise_type == C25K_FINISHED; +} +static inline bool _cleared(couchTo5k_state_t *state){ + return state->timer == C25K_SESSIONS[state->session][0] + && state->exercise == 0; +} +static inline void _next_session(couchTo5k_state_t *state){ + if (++state->session >= C25K_SESSIONS_LENGTH){ + state->session = 0; + } +} + +static inline void _assign_exercise_type(couchTo5k_state_t *state){ + if (state->exercise == 0){ + state->exercise_type = C25K_WARMUP; + } else if (state->exercise % 2 == 1){ + state->exercise_type = C25K_RUN; + } else { + state->exercise_type = C25K_WALK; + } +} + +static void _next_exercise(couchTo5k_state_t *state){ + state->exercise++; + state->timer = C25K_SESSIONS[state->session][state->exercise]; + // If the new timer starts in zero, it's finished + if (state->timer == 0){ + movement_play_alarm_beeps(7, BUZZER_NOTE_C8); + state->exercise_type = C25K_FINISHED; + return; + } + movement_play_alarm_beeps(4, BUZZER_NOTE_A7); + _assign_exercise_type(state); +} + +static void _init_session(couchTo5k_state_t *state){ + state->exercise = 0; // Restart exercise counter + state->timer = C25K_SESSIONS[state->session][state->exercise]; + _assign_exercise_type(state); +} + +static char *_exercise_type_to_str(exercise_type_t t){ + switch (t){ + case C25K_WARMUP: + return "WU"; + case C25K_RUN: + return "RU"; + case C25K_WALK: + return "WA"; + case C25K_FINISHED: + return "--"; + default: + return " "; + } +} +static void _display(couchTo5k_state_t *state, char *buf){ + // TODO only repaint needed parts + uint8_t seconds = state->timer % 60; + sprintf(buf, "%s%2d%2d%02d%02d", + _exercise_type_to_str(state->exercise_type), + (state->session + 1) % 100, + ((state->timer - seconds) / 60) % 100, + seconds, + (state->exercise + 1) % 100); + watch_display_string(buf, 0); +} + + +void couchTo5k_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(couchTo5k_state_t)); + memset(*context_ptr, 0, sizeof(couchTo5k_state_t)); + // Do any one-time tasks in here; the inside of this conditional + // happens only at boot. + // C25K_SESSIONS[0] = C25K_WEEK_TEST; + C25K_SESSIONS[0] = C25K_WEEK_1; + C25K_SESSIONS[1] = C25K_WEEK_1; + C25K_SESSIONS[2] = C25K_WEEK_1; + C25K_SESSIONS[3] = C25K_WEEK_2; + C25K_SESSIONS[4] = C25K_WEEK_2; + C25K_SESSIONS[5] = C25K_WEEK_2; + C25K_SESSIONS[6] = C25K_WEEK_3; + C25K_SESSIONS[7] = C25K_WEEK_3; + C25K_SESSIONS[8] = C25K_WEEK_3; + C25K_SESSIONS[9] = C25K_WEEK_4; + C25K_SESSIONS[10] = C25K_WEEK_4; + C25K_SESSIONS[11] = C25K_WEEK_4; + C25K_SESSIONS[12] = C25K_WEEK_5_1; + C25K_SESSIONS[13] = C25K_WEEK_5_2; + C25K_SESSIONS[14] = C25K_WEEK_5_3; + C25K_SESSIONS[15] = C25K_WEEK_6_1; + C25K_SESSIONS[16] = C25K_WEEK_6_2; + C25K_SESSIONS[17] = C25K_WEEK_6_3; + C25K_SESSIONS[18] = C25K_WEEK_7; + C25K_SESSIONS[19] = C25K_WEEK_7; + C25K_SESSIONS[20] = C25K_WEEK_7; + C25K_SESSIONS[21] = C25K_WEEK_8; + C25K_SESSIONS[22] = C25K_WEEK_8; + C25K_SESSIONS[23] = C25K_WEEK_8; + C25K_SESSIONS[24] = C25K_WEEK_9; + C25K_SESSIONS[25] = C25K_WEEK_9; + C25K_SESSIONS[26] = C25K_WEEK_9; + } + // Do any pin or peripheral setup here; this will be called whenever the + // watch wakes from deep sleep. +} + +void couchTo5k_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + // Handle any tasks related to your watch face coming on screen. + watch_set_colon(); +} + + +bool couchTo5k_face_loop(movement_event_t event, movement_settings_t *settings, + void *context) { + couchTo5k_state_t *state = (couchTo5k_state_t *)context; + static char buf[11]; + static bool paused = true; + + switch (event.event_type) { + case EVENT_ACTIVATE: + // Show your initial UI here. + movement_request_tick_frequency(1); + _init_session(state); + paused = true; + _display(state, buf); + break; + case EVENT_TICK: + if ( !paused && !_finished(state) ) { + if (state->timer == 0){ + _next_exercise(state); + } else { + state->timer--; + } + } + _display(state, buf); + break; + case EVENT_LIGHT_BUTTON_UP: + // This is the next-exercise / reset button. + + // When finished move to the next session and leave it paused + if ( _finished(state) ){ + _next_session(state); + _init_session(state); + paused = true; + break; + } + // When paused and cleared move to next, when only paused, clear + if ( paused ) { + if ( _cleared(state) ){ + _next_session(state); + } + _init_session(state); + } + break; + case EVENT_ALARM_BUTTON_UP: + paused = !paused; + break; + case EVENT_TIMEOUT: + // Your watch face will receive this event after a period of + // inactivity. If it makes sense to resign, + // movement_move_to_face(0); + break; + case EVENT_LOW_ENERGY_UPDATE: + // If you did not resign in EVENT_TIMEOUT, you can use this event + // to update the display once a minute. Avoid displaying + // fast-updating values like seconds, since the display won't + // update again for 60 seconds. You should also consider starting + // the tick animation, to show the wearer that this is sleep mode: + // watch_start_tick_animation(500); + break; + default: + // Movement's default loop handler will step in for any cases you + // don't handle above: + // * EVENT_LIGHT_BUTTON_DOWN lights the LED + // * EVENT_MODE_BUTTON_UP moves to the next watch face in the list + // * EVENT_MODE_LONG_PRESS returns to the first watch face (or + // skips to the secondary watch face, if configured) + // You can override any of these behaviors by adding a case for + // these events to this switch statement. + return movement_default_loop_handler(event, settings); + } + + // return true if the watch can enter standby mode. Generally speaking, you + // should always return true. + // Exceptions: + // * If you are displaying a color using the low-level watch_set_led_color + // function, you should return false. + // * If you are sounding the buzzer using the low-level + // watch_set_buzzer_on function, you should return false. + // Note that if you are driving the LED or buzzer using Movement functions + // like movement_illuminate_led or movement_play_alarm, you can still + // return true. This guidance only applies to the low-level watch_ + // functions. + return true; +} + +void couchTo5k_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + + // handle any cleanup before your watch face goes off-screen. +} + diff --git a/movement/watch_faces/complication/couchTo5k_face.h b/movement/watch_faces/complication/couchTo5k_face.h new file mode 100644 index 00000000..149db599 --- /dev/null +++ b/movement/watch_faces/complication/couchTo5k_face.h @@ -0,0 +1,87 @@ +/* + * MIT License + * + * Copyright (c) 2023 Ekaitz Zarraga + * + * 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 COUCHTO5K_FACE_H_ +#define COUCHTO5K_FACE_H_ + +#include "movement.h" + +/* + * Couch To 5k; + * + * + * The program is designed to train 3 times a week. Each training is a + * *session*. Each of the rounds you have in the training is an *exercise*. + * + * The training goes like this: + * 5min warm-up walk -> Run X minutes -> Walk Y minutes -> ... -> Stop + * + * The watch face shows it like this: The weekday indicator shows if you need + * to Warm Up (`WU`), run (`rU`), walk (`WA`) or stop (`--`). + * + * The month-day indicator shows the session you are in (from 1 to 27). + * + * The timer shows the time you have left in the exercise and the exercise you + * are doing (MM:SS:ee). When an exercise finishes you are notified with an + * alarm. When the whole session finishes, a different tone is played for a + * longer period. + * + * Pressing the ALARM button pauses/resumes the clock. + * + * Pressing the LIGHT button does nothing if the timer is not paused. When it + * is paused it clears the current session (it restarts it to the beginning) + * and if it was already cleared or the current session was finished moves to + * the next session. + */ + +typedef enum { + C25K_WARMUP, + C25K_RUN, + C25K_WALK, + C25K_FINISHED +} exercise_type_t; + +typedef struct { + // Anything you need to keep track of, put it here! + uint8_t session; + uint8_t exercise; + exercise_type_t exercise_type; + uint16_t timer; +} couchTo5k_state_t; + +void couchTo5k_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void couchTo5k_face_activate(movement_settings_t *settings, void *context); +bool couchTo5k_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void couchTo5k_face_resign(movement_settings_t *settings, void *context); + +#define couchTo5k_face ((const watch_face_t){ \ + couchTo5k_face_setup, \ + couchTo5k_face_activate, \ + couchTo5k_face_loop, \ + couchTo5k_face_resign, \ + NULL, \ +}) + +#endif // COUCHTO5K_FACE_H_ + From d3d76ed0e7e1ade089172631685b839a4123f45b Mon Sep 17 00:00:00 2001 From: Ekaitz Zarraga Date: Sat, 21 Oct 2023 15:47:26 +0200 Subject: [PATCH 11/45] Add sound to pause/resume button --- movement/watch_faces/complication/couchTo5k_face.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/movement/watch_faces/complication/couchTo5k_face.c b/movement/watch_faces/complication/couchTo5k_face.c index ca63b76f..79c22a5b 100644 --- a/movement/watch_faces/complication/couchTo5k_face.c +++ b/movement/watch_faces/complication/couchTo5k_face.c @@ -214,6 +214,9 @@ bool couchTo5k_face_loop(movement_event_t event, movement_settings_t *settings, } break; case EVENT_ALARM_BUTTON_UP: + if (settings->bit.button_should_sound) { + watch_buzzer_play_note(BUZZER_NOTE_C8, 50); + } paused = !paused; break; case EVENT_TIMEOUT: From 9c895817a07cfaae6e1d27fe8e84fd4a1926e960 Mon Sep 17 00:00:00 2001 From: Brian Blakley Date: Tue, 24 Oct 2023 18:20:54 -0400 Subject: [PATCH 12/45] Add minute repeater decimal face --- movement/make/Makefile | 1 + movement/movement_faces.h | 1 + .../clock/minute_repeater_decimal_face.c | 238 ++++++++++++++++++ .../clock/minute_repeater_decimal_face.h | 84 +++++++ 4 files changed, 324 insertions(+) create mode 100644 movement/watch_faces/clock/minute_repeater_decimal_face.c create mode 100644 movement/watch_faces/clock/minute_repeater_decimal_face.h diff --git a/movement/make/Makefile b/movement/make/Makefile index 625c7729..64c0364f 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -118,6 +118,7 @@ SRCS += \ ../watch_faces/complication/flashlight_face.c \ ../watch_faces/clock/decimal_time_face.c \ ../watch_faces/clock/wyoscan_face.c \ + ../watch_faces/clock/minute_repeater_decimal_face.c \ # New watch faces go above this line. # Leave this line at the bottom of the file; it has all the targets for making your project. diff --git a/movement/movement_faces.h b/movement/movement_faces.h index ff34c063..8ece8b8d 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -95,6 +95,7 @@ #include "flashlight_face.h" #include "decimal_time_face.h" #include "wyoscan_face.h" +#include "minute_repeater_decimal_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/clock/minute_repeater_decimal_face.c b/movement/watch_faces/clock/minute_repeater_decimal_face.c new file mode 100644 index 00000000..2cedc307 --- /dev/null +++ b/movement/watch_faces/clock/minute_repeater_decimal_face.c @@ -0,0 +1,238 @@ +/* + * MIT License + * + * Copyright (c) 2023 Jonas Termeau - original repetition_minute_face + * Copyright (c) 2023 Brian Blakley - modified minute_repeater_decimal_face + * + * 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. + */ + +/* + * This face, minute_repeater_decimal_face, is a modification of the original + * repetition_minute_face by Jonas Termeau. + * + * This version was created by BrianBinFL to use a decimal minute repeater pattern + * (hours, tens, and minutes) instead of the traditional pattern (hours, quarters, + * minutes). + * + * Also 500ms delays were added after the hours segment and after the tens segment + * to make it easier for the user to realize that the counting for the current + * segment has ended. + * + */ + +#include +#include "minute_repeater_decimal_face.h" +#include "watch.h" +#include "watch_utility.h" +#include "watch_private_display.h" + +void mrd_play_hour_chime(void) { + watch_buzzer_play_note(BUZZER_NOTE_C6, 75); + watch_buzzer_play_note(BUZZER_NOTE_REST, 500); +} + +void mrd_play_tens_chime(void) { + watch_buzzer_play_note(BUZZER_NOTE_E6, 75); + watch_buzzer_play_note(BUZZER_NOTE_REST, 150); + watch_buzzer_play_note(BUZZER_NOTE_C6, 75); + watch_buzzer_play_note(BUZZER_NOTE_REST, 750); +} + +void mrd_play_minute_chime(void) { + watch_buzzer_play_note(BUZZER_NOTE_E6, 75); + watch_buzzer_play_note(BUZZER_NOTE_REST, 500); +} + +static void _update_alarm_indicator(bool settings_alarm_enabled, minute_repeater_decimal_state_t *state) { + state->alarm_enabled = settings_alarm_enabled; + if (state->alarm_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL); + else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); +} + +void minute_repeater_decimal_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(minute_repeater_decimal_state_t)); + minute_repeater_decimal_state_t *state = (minute_repeater_decimal_state_t *)*context_ptr; + state->signal_enabled = false; + state->watch_face_index = watch_face_index; + } +} + +void minute_repeater_decimal_face_activate(movement_settings_t *settings, void *context) { + minute_repeater_decimal_state_t *state = (minute_repeater_decimal_state_t *)context; + + if (watch_tick_animation_is_running()) watch_stop_tick_animation(); + + if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + + // handle chime indicator + if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL); + else watch_clear_indicator(WATCH_INDICATOR_BELL); + + // show alarm indicator if there is an active alarm + _update_alarm_indicator(settings->bit.alarm_enabled, state); + + watch_set_colon(); + + // this ensures that none of the timestamp fields will match, so we can re-render them all. + state->previous_date_time = 0xFFFFFFFF; +} + +bool minute_repeater_decimal_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + minute_repeater_decimal_state_t *state = (minute_repeater_decimal_state_t *)context; + char buf[11]; + uint8_t pos; + + watch_date_time date_time; + uint32_t previous_date_time; + switch (event.event_type) { + case EVENT_ACTIVATE: + case EVENT_TICK: + case EVENT_LOW_ENERGY_UPDATE: + date_time = watch_rtc_get_date_time(); + previous_date_time = state->previous_date_time; + state->previous_date_time = date_time.reg; + + // check the battery voltage once a day... + if (date_time.unit.day != state->last_battery_check) { + state->last_battery_check = date_time.unit.day; + watch_enable_adc(); + uint16_t voltage = watch_get_vcc_voltage(); + watch_disable_adc(); + // 2.2 volts will happen when the battery has maybe 5-10% remaining? + // we can refine this later. + state->battery_low = (voltage < 2200); + } + + // ...and set the LAP indicator if low. + if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP); + + if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) { + // everything before seconds is the same, don't waste cycles setting those segments. + watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8); + watch_display_character_lp_seconds('0' + date_time.unit.second % 10, 9); + break; + } else if ((date_time.reg >> 12) == (previous_date_time >> 12) && event.event_type != EVENT_LOW_ENERGY_UPDATE) { + // everything before minutes is the same. + pos = 6; + sprintf(buf, "%02d%02d", date_time.unit.minute, date_time.unit.second); + } else { + // other stuff changed; let's do it all. + if (!settings->bit.clock_mode_24h) { + // if we are in 12 hour mode, do some cleanup. + if (date_time.unit.hour < 12) { + watch_clear_indicator(WATCH_INDICATOR_PM); + } else { + watch_set_indicator(WATCH_INDICATOR_PM); + } + date_time.unit.hour %= 12; + if (date_time.unit.hour == 0) date_time.unit.hour = 12; + } + pos = 0; + if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { + if (!watch_tick_animation_is_running()) watch_start_tick_animation(500); + sprintf(buf, "%s%2d%2d%02d ", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute); + } else { + sprintf(buf, "%s%2d%2d%02d%02d", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second); + } + } + watch_display_string(buf, pos); + // handle alarm indicator + if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state); + break; + case EVENT_ALARM_LONG_PRESS: + state->signal_enabled = !state->signal_enabled; + if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL); + else watch_clear_indicator(WATCH_INDICATOR_BELL); + break; + case EVENT_BACKGROUND_TASK: + movement_play_signal(); + break; + case EVENT_LIGHT_LONG_UP: + /* + * Howdy neighbors, this is the actual complication. Like an actual + * (very expensive) watch with a repetition minute complication it's + * boring at 00:00 or 1:00 and very quite musical at 23:59 or 12:59. + */ + + date_time = watch_rtc_get_date_time(); + + + int hours = date_time.unit.hour; + int tens = date_time.unit.minute / 10; + int minutes = date_time.unit.minute % 10; + + // chiming hours + if (!settings->bit.clock_mode_24h) { + hours = date_time.unit.hour % 12; + if (hours == 0) hours = 12; + } + if (hours > 0) { + int count = 0; + for(count = hours; count > 0; --count) { + mrd_play_hour_chime(); + } + // do a little pause before proceeding to tens + watch_buzzer_play_note(BUZZER_NOTE_REST, 500); + } + + // chiming tens (if needed) + if (tens > 0) { + int count = 0; + for(count = tens; count > 0; --count) { + mrd_play_tens_chime(); + } + // do a little pause before proceeding to minutes + watch_buzzer_play_note(BUZZER_NOTE_REST, 500); + } + + // chiming minutes (if needed) + if (minutes > 0) { + int count = 0; + for(count = minutes; count > 0; --count) { + mrd_play_minute_chime(); + } + } + + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void minute_repeater_decimal_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + +bool minute_repeater_decimal_face_wants_background_task(movement_settings_t *settings, void *context) { + (void) settings; + minute_repeater_decimal_state_t *state = (minute_repeater_decimal_state_t *)context; + if (!state->signal_enabled) return false; + + watch_date_time date_time = watch_rtc_get_date_time(); + + return date_time.unit.minute == 0; +} diff --git a/movement/watch_faces/clock/minute_repeater_decimal_face.h b/movement/watch_faces/clock/minute_repeater_decimal_face.h new file mode 100644 index 00000000..4bc9a8b6 --- /dev/null +++ b/movement/watch_faces/clock/minute_repeater_decimal_face.h @@ -0,0 +1,84 @@ +/* + * MIT License + * + * Copyright (c) 2023 Jonas Termeau - original repetition_minute_face + * Copyright (c) 2023 Brian Blakley - modified minute_repeater_decimal_face + * + * 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 MINUTE_REPEATER_DECIMAL_FACE_H_ +#define MINUTE_REPEATER_DECIMAL_FACE_H_ + +#include "movement.h" + +/* + * A hopefully useful complication for friendly neighbors in the dark + * + * Originating from 1676 from reverend and mechanician Edward Barlow, and + * perfected in 1820 by neighbor Abraham Breguet, a minute repeater or + * "repetition minute" is a complication in a mechanical watch or clock that + * chimes the hours and often minutes at the press of a button. There are many + * types of repeater, from the simple repeater which merely strikes the number + * of hours, to the minute repeater which chimes the time down to the minute, + * using separate tones for hours, decimal hours, and minutes. They originated + * before widespread artificial illumination, to allow the time to be determined + * in the dark, and were also used by the visually impaired. + * + * + * How to use it : + * + * Long press the light button to get an auditive reading of the time like so : + * 0..23 (1..12 if 24-hours format isn't enabled) low beep(s) for the hours + * 0..9 low-high couple pitched beeps for the tens of minutes + * 0..9 high pitched beep(s) for the remaining minutes (ones of minutes) + * + * Prerequisite : a watch with a working buzzer + * + * ~ Only in the darkness can you see the stars. - Martin Luther King ~ + * + */ + +typedef struct { + uint32_t previous_date_time; + uint8_t last_battery_check; + uint8_t watch_face_index; + bool signal_enabled; + bool battery_low; + bool alarm_enabled; +} minute_repeater_decimal_state_t; + +void mrd_play_hour_chime(void); +void mrd_play_tens_chime(void); +void mrd_play_minute_chime(void); +void minute_repeater_decimal_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void minute_repeater_decimal_face_activate(movement_settings_t *settings, void *context); +bool minute_repeater_decimal_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void minute_repeater_decimal_face_resign(movement_settings_t *settings, void *context); +bool minute_repeater_decimal_face_wants_background_task(movement_settings_t *settings, void *context); + +#define minute_repeater_decimal_face ((const watch_face_t){ \ + minute_repeater_decimal_face_setup, \ + minute_repeater_decimal_face_activate, \ + minute_repeater_decimal_face_loop, \ + minute_repeater_decimal_face_resign, \ + minute_repeater_decimal_face_wants_background_task, \ +}) + +#endif // MINUTE_REPEATER_DECIMAL_FACE_H_ From dcf167a8bf6f09d3068bd419bba9c92586c7cb04 Mon Sep 17 00:00:00 2001 From: Wesley Aptekar-Cassels Date: Fri, 3 Nov 2023 05:34:56 -0400 Subject: [PATCH 13/45] Add solstice_face. --- movement/make/Makefile | 1 + movement/movement_faces.h | 1 + .../watch_faces/complication/solstice_face.c | 236 ++++++++++++++++++ .../watch_faces/complication/solstice_face.h | 64 +++++ 4 files changed, 302 insertions(+) create mode 100644 movement/watch_faces/complication/solstice_face.c create mode 100644 movement/watch_faces/complication/solstice_face.h diff --git a/movement/make/Makefile b/movement/make/Makefile index 625c7729..d020fc9e 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -118,6 +118,7 @@ SRCS += \ ../watch_faces/complication/flashlight_face.c \ ../watch_faces/clock/decimal_time_face.c \ ../watch_faces/clock/wyoscan_face.c \ + ../watch_faces/complication/solstice_face.c \ # New watch faces go above this line. # Leave this line at the bottom of the file; it has all the targets for making your project. diff --git a/movement/movement_faces.h b/movement/movement_faces.h index ff34c063..2131e589 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -95,6 +95,7 @@ #include "flashlight_face.h" #include "decimal_time_face.h" #include "wyoscan_face.h" +#include "solstice_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/complication/solstice_face.c b/movement/watch_faces/complication/solstice_face.c new file mode 100644 index 00000000..e74f8789 --- /dev/null +++ b/movement/watch_faces/complication/solstice_face.c @@ -0,0 +1,236 @@ +/* + * MIT License + * + * Copyright (c) 2023 Wesley Aptekar-Cassels + * + * 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. + */ + +#include +#include +#include +#include "watch_utility.h" +#include "solstice_face.h" + +// Find solstice or equinox time in JDE for a given year, via method from Meeus Ch 27 +static double calculate_solstice_equinox(uint16_t year, uint8_t k) { + double Y = ((double)year - 2000) / 1000; + double approx_terms[4][5] = { + {2451623.80984, 365242.37404, 0.05169, -0.00411, -0.00057}, // March equinox + {2451716.56767, 365241.62603, 0.00325, 0.00888, -0.00030}, // June solstice + {2451810.21715, 365242.01767, -0.11575, 0.00337, 0.00078}, // September equinox + {2451900.05952, 365242.74049, -0.06223, -0.00823, 0.00032}, // December solstice + }; + double JDE0 = approx_terms[k][0] + Y * (approx_terms[k][1] + Y * (approx_terms[k][2] + Y * (approx_terms[k][3] + Y * approx_terms[k][4]))); + double T = (JDE0 - 2451545.0) / 36525; + double W = 35999.373 * T - 2.47; + double dlambda = 1 + (0.0334 * cos(W * M_PI / 180.0)) + (0.0007 * cos(2 * W * M_PI / 180.0)); + double correction_terms[25][3] = { + {485,324.96,1934.136}, + {203,337.23,32964.467}, + {199,342.08,20.186}, + {182,27.85,445267.112}, + {156,73.14,45036.886}, + {136,171.52,22518.443}, + {77,222.54,65928.934}, + {74,296.72,3034.906}, + {70,243.58,9037.513}, + {58,119.81,33718.147}, + {52,297.17,150.678}, + {50,21.02,2281.226}, + {45,247.54,29929.562}, + {44,325.15,31555.956}, + {29,60.93,4443.417}, + {18,155.12,67555.328}, + {17,288.79,4562.452}, + {16,198.04,62894.029}, + {14,199.76,31436.921}, + {12,95.39,14577.848}, + {12,287.11,31931.756}, + {12,320.81,34777.259}, + {9,227.73,1222.114}, + {8,15.45,16859.074}, + }; + double S = 0; + for (int i = 0; i < 25; i++) { + S += correction_terms[i][0] * cos((correction_terms[i][1] + correction_terms[i][2] * T) * M_PI / 180.0); + } + double JDE = JDE0 + (0.00001 * S) / dlambda; + + return JDE; +} + +// Convert JDE to Gergorian datetime as per Meeus Ch 7 +static watch_date_time jde_to_date_time(double JDE) { + double tmp = JDE + 0.5; + double Z = floor(tmp); + double F = fmod(tmp, 1); + double A; + if (Z < 2299161) { + A = Z; + } else { + double alpha = floor((Z - 1867216.25) / 36524.25); + A = Z + 1 + alpha - floor(alpha / 4); + } + double B = A + 1524; + double C = floor((B - 122.1) / 365.25); + double D = floor(365.25 * C); + double E = floor((B - D) / 30.6001); + double day = B - D - floor(30.6001 * E) + F; + double month; + if (E < 14) { + month = E - 1; + } else { + month = E - 13; + } + double year; + if (month > 2) { + year = C - 4716; + } else { + year = C - 4715; + } + + double hours = fmod(day, 1) * 24; + double minutes = fmod(hours, 1) * 60; + double seconds = fmod(minutes, 1) * 60; + + watch_date_time result = {.unit = { + floor(seconds), + floor(minutes), + floor(hours), + floor(day), + floor(month), + floor(year - 2020) + }}; + + return result; +} + +static void calculate_datetimes(solstice_state_t *state, movement_settings_t *settings) { + for (int i = 0; i < 4; i++) { + // TODO: handle DST changes + state->datetimes[i] = jde_to_date_time(calculate_solstice_equinox(2020 + state->year, i) + (movement_timezone_offsets[settings->bit.time_zone] / (60.0*24.0))); + } +} + +void solstice_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(solstice_state_t)); + solstice_state_t *state = (solstice_state_t *)*context_ptr; + + watch_date_time now = watch_rtc_get_date_time(); + state->year = now.unit.year; + state->index = 0; + calculate_datetimes(state, settings); + + uint32_t now_unix = watch_utility_date_time_to_unix_time(now, 0); + for (int i = 0; i < 4; i++) { + if (state->index == 0 && watch_utility_date_time_to_unix_time(state->datetimes[i], 0) > now_unix) { + state->index = i; + } + } + } +} + +void solstice_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + +static void show_main_screen(solstice_state_t *state) { + char buf[11]; + watch_date_time date_time = state->datetimes[state->index]; + sprintf(buf, " %2d %2d%02d", date_time.unit.year + 20, date_time.unit.month, date_time.unit.day); + watch_display_string(buf, 0); +} + +static void show_date_time(movement_settings_t *settings, solstice_state_t *state) { + char buf[11]; + watch_date_time date_time = state->datetimes[state->index]; + if (!settings->bit.clock_mode_24h) { + if (date_time.unit.hour < 12) { + watch_clear_indicator(WATCH_INDICATOR_PM); + } else { + watch_set_indicator(WATCH_INDICATOR_PM); + } + date_time.unit.hour %= 12; + if (date_time.unit.hour == 0) date_time.unit.hour = 12; + } + sprintf(buf, "%s%2d%2d%02d%02d", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second); + watch_set_colon(); + watch_display_string(buf, 0); +} + +bool solstice_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + solstice_state_t *state = (solstice_state_t *)context; + + switch (event.event_type) { + case EVENT_ALARM_LONG_PRESS: + show_date_time(settings, state); + break; + case EVENT_LIGHT_BUTTON_UP: + if (state->index == 0) { + if (state->year == 0) { + break; + } + state->year--; + state->index = 3; + calculate_datetimes(state, settings); + } else { + state->index--; + } + show_main_screen(state); + break; + case EVENT_ALARM_BUTTON_UP: + state->index++; + if (state->index > 3) { + if (state->year == 83) { + break; + } + state->year++; + state->index = 0; + calculate_datetimes(state, settings); + } + show_main_screen(state); + break; + case EVENT_ALARM_LONG_UP: + watch_clear_colon(); + watch_clear_indicator(WATCH_INDICATOR_PM); + show_main_screen(state); + break; + case EVENT_ACTIVATE: + show_main_screen(state); + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void solstice_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + diff --git a/movement/watch_faces/complication/solstice_face.h b/movement/watch_faces/complication/solstice_face.h new file mode 100644 index 00000000..ec537c09 --- /dev/null +++ b/movement/watch_faces/complication/solstice_face.h @@ -0,0 +1,64 @@ +/* + * MIT License + * + * Copyright (c) 2023 Wesley Aptekar-Cassels + * + * 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 SOLSTICE_FACE_H_ +#define SOLSTICE_FACE_H_ + +#include "movement.h" + +/* + * A face for telling the dates and times of solstices and equinoxes + * + * It shows the upcoming solstice or equinox by default. The upper right number + * is the year, and the bottom numbers are the date in MMDD format. Use the + * alarm / light buttons to go forwards / backwards in time. Long press the + * alarm button to show the time of the event, including what weekday it is on, + * in your local timezone (DST is not handled). + * + * Supports the years 2020 - 2083. The calculations are reasonably accurate for + * years between 1000 and 3000, but limitations in the sensor watch libraries + * (which could easily be worked around) prevent making use of that. + */ + +typedef struct { + watch_date_time datetimes[4]; + uint8_t year; + uint8_t index; +} solstice_state_t; + +void solstice_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void solstice_face_activate(movement_settings_t *settings, void *context); +bool solstice_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void solstice_face_resign(movement_settings_t *settings, void *context); + +#define solstice_face ((const watch_face_t){ \ + solstice_face_setup, \ + solstice_face_activate, \ + solstice_face_loop, \ + solstice_face_resign, \ + NULL, \ +}) + +#endif // SOLSTICE_FACE_H_ + From 73f689e65ac11da126c54b3b2252ffac37e796cc Mon Sep 17 00:00:00 2001 From: Wesley Aptekar-Cassels Date: Wed, 8 Nov 2023 19:22:03 -0500 Subject: [PATCH 14/45] Add simple_coin_flip_face. --- movement/make/Makefile | 1 + movement/movement_faces.h | 1 + .../complication/simple_coin_flip_face.c | 139 ++++++++++++++++++ .../complication/simple_coin_flip_face.h | 62 ++++++++ 4 files changed, 203 insertions(+) create mode 100644 movement/watch_faces/complication/simple_coin_flip_face.c create mode 100644 movement/watch_faces/complication/simple_coin_flip_face.h diff --git a/movement/make/Makefile b/movement/make/Makefile index 625c7729..35f610a5 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -118,6 +118,7 @@ SRCS += \ ../watch_faces/complication/flashlight_face.c \ ../watch_faces/clock/decimal_time_face.c \ ../watch_faces/clock/wyoscan_face.c \ + ../watch_faces/complication/simple_coin_flip_face.c \ # New watch faces go above this line. # Leave this line at the bottom of the file; it has all the targets for making your project. diff --git a/movement/movement_faces.h b/movement/movement_faces.h index ff34c063..0d7227a3 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -95,6 +95,7 @@ #include "flashlight_face.h" #include "decimal_time_face.h" #include "wyoscan_face.h" +#include "simple_coin_flip_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/complication/simple_coin_flip_face.c b/movement/watch_faces/complication/simple_coin_flip_face.c new file mode 100644 index 00000000..64431f9d --- /dev/null +++ b/movement/watch_faces/complication/simple_coin_flip_face.c @@ -0,0 +1,139 @@ +/* + * MIT License + * + * Copyright (c) 2023 Wesley Aptekar-Cassels + * + * 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. + */ + +#if __EMSCRIPTEN__ +#include +#else +#include "saml22j18a.h" +#endif + +#include +#include +#include "simple_coin_flip_face.h" + +#define SIMPLE_COIN_FLIP_REQUIRE_LONG_PRESS_FOR_REFLIP true + +void simple_coin_flip_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(simple_coin_flip_state_t)); + memset(*context_ptr, 0, sizeof(simple_coin_flip_state_t)); + } +} + +void simple_coin_flip_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + +static uint32_t get_random(uint32_t max) { + #if __EMSCRIPTEN__ + return rand() % max; + #else + return arc4random_uniform(max); + #endif +} + +static void animation_0() { + watch_display_string(" ", 8); + watch_set_pixel(0, 3); + watch_set_pixel(0, 6); +} + +static void animation_1() { + watch_display_string(" ", 8); + watch_set_pixel(1, 3); + watch_set_pixel(1, 5); +} + +static void animation_2() { + watch_display_string(" ", 8); + watch_set_pixel(2, 2); + watch_set_pixel(2, 4); +} + +bool simple_coin_flip_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + simple_coin_flip_state_t *state = (simple_coin_flip_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + watch_display_string("flip", 5); + state->animation_frame = 0; + break; + case EVENT_TICK: + switch (state->animation_frame) { + case 0: + case 7: + return true; + case 1: + movement_request_tick_frequency(8); + watch_display_string(" ", 4); + // fallthrough + case 5: + animation_0(); + break; + case 2: + case 4: + animation_1(); + break; + case 3: + animation_2(); + break; + case 6: + movement_request_tick_frequency(1); + if (get_random(2)) { + watch_display_string("Heads ", 4); + } else { + watch_display_string(" Tails", 4); + } + break; + } + state->animation_frame++; + break; + case EVENT_LIGHT_BUTTON_UP: + case EVENT_ALARM_BUTTON_UP: + if (!SIMPLE_COIN_FLIP_REQUIRE_LONG_PRESS_FOR_REFLIP || state->animation_frame == 0) { + state->animation_frame = 1; + } + break; + case EVENT_ALARM_LONG_PRESS: + case EVENT_LIGHT_LONG_PRESS: + state->animation_frame = 1; + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void simple_coin_flip_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + diff --git a/movement/watch_faces/complication/simple_coin_flip_face.h b/movement/watch_faces/complication/simple_coin_flip_face.h new file mode 100644 index 00000000..f5e223b8 --- /dev/null +++ b/movement/watch_faces/complication/simple_coin_flip_face.h @@ -0,0 +1,62 @@ +/* + * MIT License + * + * Copyright (c) 2023 Wesley Aptekar-Cassels + * + * 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 SIMPLE_COIN_FLIP_FACE_H_ +#define SIMPLE_COIN_FLIP_FACE_H_ + +#include "movement.h" + +/* + * A extremely simple coin flip face. + * + * Press ALARM or LIGHT to flip a coin, after a short animation it will display + * "Heads" or "Tails". Long-press to flip again (you can change a #define to + * allow a short-press to reflip as well, if you'd like). + * + * This is for people who want a simpler UI than probability_face or + * randonaut_face. While those have more features, this one is more immediately + * obvious - useful, for instance, if you are using a coin flip to agree on + * something with someone, and want the operation to be clear to someone who + * has not had anything explained to them. + */ + +typedef struct { + uint8_t animation_frame; +} simple_coin_flip_state_t; + +void simple_coin_flip_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void simple_coin_flip_face_activate(movement_settings_t *settings, void *context); +bool simple_coin_flip_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void simple_coin_flip_face_resign(movement_settings_t *settings, void *context); + +#define simple_coin_flip_face ((const watch_face_t){ \ + simple_coin_flip_face_setup, \ + simple_coin_flip_face_activate, \ + simple_coin_flip_face_loop, \ + simple_coin_flip_face_resign, \ + NULL, \ +}) + +#endif // SIMPLE_COIN_FLIP_FACE_H_ + From f1d4d4ce89e46a02b719baf1f33947a6ef53b23f Mon Sep 17 00:00:00 2001 From: Wesley Aptekar-Cassels Date: Thu, 9 Nov 2023 00:03:28 -0500 Subject: [PATCH 15/45] Add day_night_percentage_face. --- movement/make/Makefile | 1 + movement/movement_faces.h | 1 + .../clock/day_night_percentage_face.c | 121 ++++++++++++++++++ .../clock/day_night_percentage_face.h | 61 +++++++++ 4 files changed, 184 insertions(+) create mode 100644 movement/watch_faces/clock/day_night_percentage_face.c create mode 100644 movement/watch_faces/clock/day_night_percentage_face.h diff --git a/movement/make/Makefile b/movement/make/Makefile index 625c7729..3953e31e 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -118,6 +118,7 @@ SRCS += \ ../watch_faces/complication/flashlight_face.c \ ../watch_faces/clock/decimal_time_face.c \ ../watch_faces/clock/wyoscan_face.c \ + ../watch_faces/clock/day_night_percentage_face.c \ # New watch faces go above this line. # Leave this line at the bottom of the file; it has all the targets for making your project. diff --git a/movement/movement_faces.h b/movement/movement_faces.h index ff34c063..db4f0a2d 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -95,6 +95,7 @@ #include "flashlight_face.h" #include "decimal_time_face.h" #include "wyoscan_face.h" +#include "day_night_percentage_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/clock/day_night_percentage_face.c b/movement/watch_faces/clock/day_night_percentage_face.c new file mode 100644 index 00000000..ed62653f --- /dev/null +++ b/movement/watch_faces/clock/day_night_percentage_face.c @@ -0,0 +1,121 @@ +/* + * MIT License + * + * Copyright (c) 2023 Wesley Aptekar-Cassels + * + * 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. + */ + +#include +#include +#include +#include "day_night_percentage_face.h" +#include "watch_utility.h" +#include "sunriset.h" + +void day_night_percentage_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (void) watch_face_index; + (void) context_ptr; +} + +void day_night_percentage_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + +// fmod but handle negatives right +static double better_fmod(double x, double y) { + return fmod(fmod(x, y) + y, y); +} + +bool day_night_percentage_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + (void) context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + case EVENT_TICK: + case EVENT_LOW_ENERGY_UPDATE: + { + movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1); + + if (movement_location.reg == 0) { + watch_display_string(" no Loc", 0); + return true; + } + + watch_date_time date_time = watch_rtc_get_date_time(); // the current local date / time + watch_date_time utc_now = watch_utility_date_time_convert_zone(date_time, movement_timezone_offsets[settings->bit.time_zone] * 60, 0); // the current date / time in UTC + + // Weird quirky unsigned things were happening when I tried to cast these directly to doubles below. + // it looks redundant, but extracting them to local int16's seemed to fix it. + int16_t lat_centi = (int16_t)movement_location.bit.latitude; + int16_t lon_centi = (int16_t)movement_location.bit.longitude; + + double lat = (double)lat_centi / 100.0; + double lon = (double)lon_centi / 100.0; + + double daylen = day_length(utc_now.unit.year + WATCH_RTC_REFERENCE_YEAR, utc_now.unit.month, utc_now.unit.day, lon, lat); + + double rise, set; + char buf[12]; + + int result = sun_rise_set(utc_now.unit.year + WATCH_RTC_REFERENCE_YEAR, utc_now.unit.month, utc_now.unit.day, lon, lat, &rise, &set); + + if (result != 0) { + sprintf(buf, "%s%2dEtrnal", result == 1 ? "DA" : "NI", date_time.unit.day); + watch_display_string(buf, 0); + } else { + double day_hours_decimal = utc_now.unit.hour + (utc_now.unit.minute + (utc_now.unit.second / 60.0)) / 60.0; + + double day_percentage = (24.0 - better_fmod(rise - day_hours_decimal, 24.0)) / daylen; + double night_percentage = (24.0 - better_fmod(set - day_hours_decimal, 24.0)) / (24 - daylen); + + uint16_t percentage; + char day_night[3]; + if (day_percentage > 0.0 && day_percentage < 1.0) { + percentage = day_percentage * 10000; + sprintf(day_night, "%s", "DA"); + } else { + percentage = night_percentage * 10000; + sprintf(day_night, "%s", "NI"); + } + if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { + if (!watch_tick_animation_is_running()) watch_start_tick_animation(500); + sprintf(buf, "%s%2d %02d", day_night, date_time.unit.day, percentage / 100); + } else { + sprintf(buf, "%s%2d %04d", day_night, date_time.unit.day, percentage); + } + watch_display_string(buf, 0); + } + + break; + } + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void day_night_percentage_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + diff --git a/movement/watch_faces/clock/day_night_percentage_face.h b/movement/watch_faces/clock/day_night_percentage_face.h new file mode 100644 index 00000000..9155745b --- /dev/null +++ b/movement/watch_faces/clock/day_night_percentage_face.h @@ -0,0 +1,61 @@ +/* + * MIT License + * + * Copyright (c) 2023 Wesley Aptekar-Cassels + * + * 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 DAY_NIGHT_PERCENTAGE_FACE_H_ +#define DAY_NIGHT_PERCENTAGE_FACE_H_ + +#include "movement.h" + +/* + * Day/night percentage face + * + * Shows the percentage of the way through the day/night the current time is. + * + * The weekday digits show "DA" or "NI" depending on whether it's currently day + * or night. The day digits show what the current day of the month is. The time + * digits show the percentage of the way through the day/night it is, with + * decimals in the smaller seconds digits. If the day or night will last for a + * full 24 hours, the text "Etrnal" is displayed instead of a percentage. + * + * This face does not currently offer any configuration. You must set the + * location register with some other face. + */ + +typedef struct {} day_night_percentage_state_t; + +void day_night_percentage_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void day_night_percentage_face_activate(movement_settings_t *settings, void *context); +bool day_night_percentage_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void day_night_percentage_face_resign(movement_settings_t *settings, void *context); + +#define day_night_percentage_face ((const watch_face_t){ \ + day_night_percentage_face_setup, \ + day_night_percentage_face_activate, \ + day_night_percentage_face_loop, \ + day_night_percentage_face_resign, \ + NULL, \ +}) + +#endif // DAY_NIGHT_PERCENTAGE_FACE_H_ + From 2e8ee9965e843f9fc571681d7f3911fd68f36c7b Mon Sep 17 00:00:00 2001 From: Wesley Aptekar-Cassels Date: Thu, 9 Nov 2023 17:21:35 -0500 Subject: [PATCH 16/45] day_night_percentage_face: Calculate rise/set/daylen only once per day. --- .../clock/day_night_percentage_face.c | 83 +++++++++++-------- .../clock/day_night_percentage_face.h | 7 +- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/movement/watch_faces/clock/day_night_percentage_face.c b/movement/watch_faces/clock/day_night_percentage_face.c index ed62653f..eaeeeb91 100644 --- a/movement/watch_faces/clock/day_night_percentage_face.c +++ b/movement/watch_faces/clock/day_night_percentage_face.c @@ -29,10 +29,42 @@ #include "watch_utility.h" #include "sunriset.h" +// fmod but handle negatives right +static double better_fmod(double x, double y) { + return fmod(fmod(x, y) + y, y); +} + +static void recalculate(watch_date_time utc_now, day_night_percentage_state_t *state) { + movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1); + + if (movement_location.reg == 0) { + state->result = -2; + return; + } + + // Weird quirky unsigned things were happening when I tried to cast these directly to doubles below. + // it looks redundant, but extracting them to local int16's seemed to fix it. + int16_t lat_centi = (int16_t)movement_location.bit.latitude; + int16_t lon_centi = (int16_t)movement_location.bit.longitude; + + double lat = (double)lat_centi / 100.0; + double lon = (double)lon_centi / 100.0; + + state->daylen = day_length(utc_now.unit.year + WATCH_RTC_REFERENCE_YEAR, utc_now.unit.month, utc_now.unit.day, lon, lat); + + state->result = sun_rise_set(utc_now.unit.year + WATCH_RTC_REFERENCE_YEAR, utc_now.unit.month, utc_now.unit.day, lon, lat, &state->rise, &state->set); +} + void day_night_percentage_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { (void) settings; (void) watch_face_index; - (void) context_ptr; + + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(day_night_percentage_state_t)); + day_night_percentage_state_t *state = (day_night_percentage_state_t *)*context_ptr; + watch_date_time utc_now = watch_utility_date_time_convert_zone(watch_rtc_get_date_time(), movement_timezone_offsets[settings->bit.time_zone] * 60, 0); + recalculate(utc_now, state); + } } void day_night_percentage_face_activate(movement_settings_t *settings, void *context) { @@ -40,52 +72,34 @@ void day_night_percentage_face_activate(movement_settings_t *settings, void *con (void) context; } -// fmod but handle negatives right -static double better_fmod(double x, double y) { - return fmod(fmod(x, y) + y, y); -} - bool day_night_percentage_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { - (void) context; + day_night_percentage_state_t *state = (day_night_percentage_state_t *)context; + + char buf[12]; + watch_date_time date_time = watch_rtc_get_date_time(); + watch_date_time utc_now = watch_utility_date_time_convert_zone(date_time, movement_timezone_offsets[settings->bit.time_zone] * 60, 0); switch (event.event_type) { case EVENT_ACTIVATE: case EVENT_TICK: case EVENT_LOW_ENERGY_UPDATE: - { - movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1); - - if (movement_location.reg == 0) { - watch_display_string(" no Loc", 0); - return true; + if ((utc_now.unit.hour == 0 && utc_now.unit.minute == 0 && utc_now.unit.second == 0) || state->result == -2) { + recalculate(utc_now, state); } - watch_date_time date_time = watch_rtc_get_date_time(); // the current local date / time - watch_date_time utc_now = watch_utility_date_time_convert_zone(date_time, movement_timezone_offsets[settings->bit.time_zone] * 60, 0); // the current date / time in UTC + if (state->result == -2) { + watch_display_string(" no Loc", 0); + break; + } - // Weird quirky unsigned things were happening when I tried to cast these directly to doubles below. - // it looks redundant, but extracting them to local int16's seemed to fix it. - int16_t lat_centi = (int16_t)movement_location.bit.latitude; - int16_t lon_centi = (int16_t)movement_location.bit.longitude; - - double lat = (double)lat_centi / 100.0; - double lon = (double)lon_centi / 100.0; - - double daylen = day_length(utc_now.unit.year + WATCH_RTC_REFERENCE_YEAR, utc_now.unit.month, utc_now.unit.day, lon, lat); - - double rise, set; - char buf[12]; - - int result = sun_rise_set(utc_now.unit.year + WATCH_RTC_REFERENCE_YEAR, utc_now.unit.month, utc_now.unit.day, lon, lat, &rise, &set); - - if (result != 0) { - sprintf(buf, "%s%2dEtrnal", result == 1 ? "DA" : "NI", date_time.unit.day); + if (state->result != 0) { + sprintf(buf, "%s%2dEtrnal", state->result == 1 ? "DA" : "NI", date_time.unit.day); watch_display_string(buf, 0); } else { double day_hours_decimal = utc_now.unit.hour + (utc_now.unit.minute + (utc_now.unit.second / 60.0)) / 60.0; - double day_percentage = (24.0 - better_fmod(rise - day_hours_decimal, 24.0)) / daylen; - double night_percentage = (24.0 - better_fmod(set - day_hours_decimal, 24.0)) / (24 - daylen); + double day_percentage = (24.0 - better_fmod(state->rise - day_hours_decimal, 24.0)) / state->daylen; + double night_percentage = (24.0 - better_fmod(state->set - day_hours_decimal, 24.0)) / (24 - state->daylen); uint16_t percentage; char day_night[3]; @@ -106,7 +120,6 @@ bool day_night_percentage_face_loop(movement_event_t event, movement_settings_t } break; - } default: return movement_default_loop_handler(event, settings); } diff --git a/movement/watch_faces/clock/day_night_percentage_face.h b/movement/watch_faces/clock/day_night_percentage_face.h index 9155745b..c5865329 100644 --- a/movement/watch_faces/clock/day_night_percentage_face.h +++ b/movement/watch_faces/clock/day_night_percentage_face.h @@ -42,7 +42,12 @@ * location register with some other face. */ -typedef struct {} day_night_percentage_state_t; +typedef struct { + int result; // -1, 0, 1: result from sun_rise_set, -2: no location set + double rise; + double set; + double daylen; +} day_night_percentage_state_t; void day_night_percentage_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); void day_night_percentage_face_activate(movement_settings_t *settings, void *context); From 26f63dcaebb375dd7fb756cc482d321da529d92f Mon Sep 17 00:00:00 2001 From: Wesley Aptekar-Cassels Date: Thu, 9 Nov 2023 17:33:43 -0500 Subject: [PATCH 17/45] day_night_percentage_face: Use PM indicator instead of DA/NI. This allows for use of the weekday digits for displaying the weekday. --- .../clock/day_night_percentage_face.c | 17 +++++++++++------ .../clock/day_night_percentage_face.h | 10 +++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/movement/watch_faces/clock/day_night_percentage_face.c b/movement/watch_faces/clock/day_night_percentage_face.c index eaeeeb91..02062064 100644 --- a/movement/watch_faces/clock/day_night_percentage_face.c +++ b/movement/watch_faces/clock/day_night_percentage_face.c @@ -92,8 +92,14 @@ bool day_night_percentage_face_loop(movement_event_t event, movement_settings_t break; } + const char* weekday = watch_utility_get_weekday(date_time); if (state->result != 0) { - sprintf(buf, "%s%2dEtrnal", state->result == 1 ? "DA" : "NI", date_time.unit.day); + if (state->result == 1) { + watch_clear_indicator(WATCH_INDICATOR_PM); + } else { + watch_set_indicator(WATCH_INDICATOR_PM); + } + sprintf(buf, "%s%2dEtrnal", weekday, date_time.unit.day); watch_display_string(buf, 0); } else { double day_hours_decimal = utc_now.unit.hour + (utc_now.unit.minute + (utc_now.unit.second / 60.0)) / 60.0; @@ -102,19 +108,18 @@ bool day_night_percentage_face_loop(movement_event_t event, movement_settings_t double night_percentage = (24.0 - better_fmod(state->set - day_hours_decimal, 24.0)) / (24 - state->daylen); uint16_t percentage; - char day_night[3]; if (day_percentage > 0.0 && day_percentage < 1.0) { percentage = day_percentage * 10000; - sprintf(day_night, "%s", "DA"); + watch_clear_indicator(WATCH_INDICATOR_PM); } else { percentage = night_percentage * 10000; - sprintf(day_night, "%s", "NI"); + watch_set_indicator(WATCH_INDICATOR_PM); } if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { if (!watch_tick_animation_is_running()) watch_start_tick_animation(500); - sprintf(buf, "%s%2d %02d", day_night, date_time.unit.day, percentage / 100); + sprintf(buf, "%s%2d %02d", weekday, date_time.unit.day, percentage / 100); } else { - sprintf(buf, "%s%2d %04d", day_night, date_time.unit.day, percentage); + sprintf(buf, "%s%2d %04d", weekday, date_time.unit.day, percentage); } watch_display_string(buf, 0); } diff --git a/movement/watch_faces/clock/day_night_percentage_face.h b/movement/watch_faces/clock/day_night_percentage_face.h index c5865329..d172c748 100644 --- a/movement/watch_faces/clock/day_night_percentage_face.h +++ b/movement/watch_faces/clock/day_night_percentage_face.h @@ -32,11 +32,11 @@ * * Shows the percentage of the way through the day/night the current time is. * - * The weekday digits show "DA" or "NI" depending on whether it's currently day - * or night. The day digits show what the current day of the month is. The time - * digits show the percentage of the way through the day/night it is, with - * decimals in the smaller seconds digits. If the day or night will last for a - * full 24 hours, the text "Etrnal" is displayed instead of a percentage. + * The time digits show the percentage of the way through the day/night it is, + * with decimals in the smaller seconds digits. If the day or night will last + * for a full 24 hours, the text "Etrnal" is displayed instead of a percentage. + * The "PM" indicator is set when it is currently nighttime. The weekday and + * day digits display the weekday and day, as one would expect. * * This face does not currently offer any configuration. You must set the * location register with some other face. From 691f2fe5060c25c12471b185af5cef7f8e4d57e6 Mon Sep 17 00:00:00 2001 From: Wesley Aptekar-Cassels Date: Thu, 9 Nov 2023 20:31:56 -0500 Subject: [PATCH 18/45] day_night_percentage_face: Clear seconds digits when entering LE mode. --- movement/watch_faces/clock/day_night_percentage_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/movement/watch_faces/clock/day_night_percentage_face.c b/movement/watch_faces/clock/day_night_percentage_face.c index 02062064..86f07f9d 100644 --- a/movement/watch_faces/clock/day_night_percentage_face.c +++ b/movement/watch_faces/clock/day_night_percentage_face.c @@ -117,7 +117,7 @@ bool day_night_percentage_face_loop(movement_event_t event, movement_settings_t } if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { if (!watch_tick_animation_is_running()) watch_start_tick_animation(500); - sprintf(buf, "%s%2d %02d", weekday, date_time.unit.day, percentage / 100); + sprintf(buf, "%s%2d %02d ", weekday, date_time.unit.day, percentage / 100); } else { sprintf(buf, "%s%2d %04d", weekday, date_time.unit.day, percentage); } From 8c7e9fa558917030a7550440aacebd7a207a5591 Mon Sep 17 00:00:00 2001 From: Christian Buschau Date: Tue, 19 Sep 2023 15:21:00 +0200 Subject: [PATCH 19/45] day_one_face: cleanup --- .../watch_faces/complication/day_one_face.c | 45 ++++++++++--------- .../watch_faces/complication/day_one_face.h | 11 ++++- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/movement/watch_faces/complication/day_one_face.c b/movement/watch_faces/complication/day_one_face.c index 25ce1c23..e7602995 100644 --- a/movement/watch_faces/complication/day_one_face.c +++ b/movement/watch_faces/complication/day_one_face.c @@ -32,15 +32,15 @@ static uint32_t _day_one_face_juliandaynum(uint16_t year, uint16_t month, uint16 return (1461 * (year + 4800 + (month - 14) / 12)) / 4 + (367 * (month - 2 - 12 * ((month - 14) / 12))) / 12 - (3 * ((year + 4900 + (month - 14) / 12) / 100))/4 + day - 32075; } -static void _day_one_face_update(day_one_state_t state) { +static void _day_one_face_update(day_one_state_t *state) { char buf[15]; watch_date_time date_time = watch_rtc_get_date_time(); uint32_t julian_date = _day_one_face_juliandaynum(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR, date_time.unit.month, date_time.unit.day); - uint32_t julian_birthdate = _day_one_face_juliandaynum(state.birth_year, state.birth_month, state.birth_day); + uint32_t julian_birthdate = _day_one_face_juliandaynum(state->birth_year, state->birth_month, state->birth_day); if (julian_date < julian_birthdate) { - sprintf(buf, "DA %6lu", julian_birthdate - julian_date); + sprintf(buf, "DA %6lu", julian_birthdate - julian_date); } else { - sprintf(buf, "DA %6lu", julian_date - julian_birthdate); + sprintf(buf, "DA %6lu", julian_date - julian_birthdate); } watch_display_string(buf, 0); } @@ -71,8 +71,7 @@ void day_one_face_activate(movement_settings_t *settings, void *context) { // stash the current year, useful in birthday setting mode. watch_date_time date_time = watch_rtc_get_date_time(); state->current_year = date_time.unit.year + WATCH_RTC_REFERENCE_YEAR; - // reset the current page to 0, display days alive. - state->current_page = 0; + state->current_page = PAGE_DISPLAY; // fetch the user's birth date from the birthday register. movement_birthdate_t movement_birthdate = (movement_birthdate_t) watch_get_backup_data(2); @@ -90,91 +89,95 @@ bool day_one_face_loop(movement_event_t event, movement_settings_t *settings, vo switch (event.event_type) { case EVENT_ACTIVATE: - _day_one_face_update(*state); + _day_one_face_update(state); break; case EVENT_LOW_ENERGY_UPDATE: case EVENT_TICK: - if (state->current_page != 0) { + if (state->current_page != PAGE_DISPLAY) { // if in settings mode, update whatever the current page is switch (state->current_page) { - case 1: + case PAGE_YEAR: watch_display_string("YR ", 0); if (event.subsecond % 2) { sprintf(buf, "%4d", state->birth_year); watch_display_string(buf, 4); } break; - case 2: + case PAGE_MONTH: watch_display_string("MO ", 0); if (event.subsecond % 2) { sprintf(buf, "%2d", state->birth_month); watch_display_string(buf, 4); } break; - case 3: + case PAGE_DAY: watch_display_string("DA ", 0); if (event.subsecond % 2) { sprintf(buf, "%2d", state->birth_day); watch_display_string(buf, 6); } break; + default: + break; } } else { // otherwise, check if we have to update. the display only needs to change at midnight! watch_date_time date_time = watch_rtc_get_date_time(); if (date_time.unit.hour == 0 && date_time.unit.minute == 0 && date_time.unit.second == 0) { - _day_one_face_update(*state); + _day_one_face_update(state); } } break; case EVENT_LIGHT_BUTTON_DOWN: // only illuminate if we're in display mode - if (state->current_page == 0) movement_illuminate_led(); + if (state->current_page == PAGE_DISPLAY) movement_illuminate_led(); break; case EVENT_LIGHT_BUTTON_UP: // otherwise use the light button to advance settings pages. - if (state->current_page != 0) { + if (state->current_page != PAGE_DISPLAY) { // go to next setting page... state->current_page = (state->current_page + 1) % 4; if (state->current_page == 0) { // ...unless we've been pushed back to display mode. movement_request_tick_frequency(1); // force display since it normally won't update til midnight. - _day_one_face_update(*state); + _day_one_face_update(state); } } break; case EVENT_ALARM_BUTTON_UP: // if we are on a settings page, increment whatever value we're setting. - if (state->current_page != 0) { + if (state->current_page != PAGE_DISPLAY) { state->birthday_changed = true; switch (state->current_page) { - case 1: + case PAGE_YEAR: state->birth_year = state->birth_year + 1; if (state->birth_year > state->current_year) state->birth_year = 1900; break; - case 2: + case PAGE_MONTH: state->birth_month = (state->birth_month % 12) + 1; break; - case 3: + case PAGE_DAY: state->birth_day = state->birth_day + 1; if (state->birth_day == 0 || state->birth_day > days_in_month[state->birth_month - 1]) { state->birth_day = 1; } break; + default: + break; } } break; case EVENT_ALARM_LONG_PRESS: // if we aren't already in settings mode, put us there. - if (state->current_page == 0) { + if (state->current_page == PAGE_DISPLAY) { state->current_page++; movement_request_tick_frequency(4); } break; case EVENT_TIMEOUT: // return home if we're on a settings page (this saves our changes when we resign). - if (state->current_page != 0) { + if (state->current_page != PAGE_DISPLAY) { movement_move_to_face(0); } break; diff --git a/movement/watch_faces/complication/day_one_face.h b/movement/watch_faces/complication/day_one_face.h index ab8372bf..5d822a5e 100644 --- a/movement/watch_faces/complication/day_one_face.h +++ b/movement/watch_faces/complication/day_one_face.h @@ -27,11 +27,18 @@ #include "movement.h" -// The Day One face is designed to count upwards from the wearer's date of birth. It also functions as an +// The Day One face is designed to count the days since or until a given date. It also functions as an // interface for setting the birth date register, which other watch faces can use for various purposes. +typedef enum { + PAGE_DISPLAY, + PAGE_YEAR, + PAGE_MONTH, + PAGE_DAY +} day_one_page_t; + typedef struct { - uint8_t current_page; + day_one_page_t current_page; uint16_t current_year; uint16_t birth_year; uint8_t birth_month; From 1022359252742c8950a403348310fc66f0e59787 Mon Sep 17 00:00:00 2001 From: Christian Buschau Date: Tue, 19 Sep 2023 17:53:07 +0200 Subject: [PATCH 20/45] day_one_face: allow years until 2080 This is the same limit introduced in commit 7fd51ca --- movement/watch_faces/complication/day_one_face.c | 9 +++------ movement/watch_faces/complication/day_one_face.h | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/movement/watch_faces/complication/day_one_face.c b/movement/watch_faces/complication/day_one_face.c index e7602995..c2c3d210 100644 --- a/movement/watch_faces/complication/day_one_face.c +++ b/movement/watch_faces/complication/day_one_face.c @@ -54,7 +54,7 @@ void day_one_face_setup(movement_settings_t *settings, uint8_t watch_face_index, movement_birthdate_t movement_birthdate = (movement_birthdate_t) watch_get_backup_data(2); if (movement_birthdate.reg == 0) { // if birth date is totally blank, set a reasonable starting date. this works well for anyone under 63, but - // you can keep pressing to go back to 1900; just pass the current year. also picked this date because if you + // you can keep pressing to go back to 1900; just pass the year 2080. also picked this date because if you // set it to 1959-01-02, it counts up from the launch of Luna-1, the first spacecraft to leave the well. movement_birthdate.bit.year = 1959; movement_birthdate.bit.month = 1; @@ -68,9 +68,6 @@ void day_one_face_activate(movement_settings_t *settings, void *context) { (void) settings; day_one_state_t *state = (day_one_state_t *)context; - // stash the current year, useful in birthday setting mode. - watch_date_time date_time = watch_rtc_get_date_time(); - state->current_year = date_time.unit.year + WATCH_RTC_REFERENCE_YEAR; state->current_page = PAGE_DISPLAY; // fetch the user's birth date from the birthday register. @@ -137,7 +134,7 @@ bool day_one_face_loop(movement_event_t event, movement_settings_t *settings, vo if (state->current_page != PAGE_DISPLAY) { // go to next setting page... state->current_page = (state->current_page + 1) % 4; - if (state->current_page == 0) { + if (state->current_page == PAGE_DISPLAY) { // ...unless we've been pushed back to display mode. movement_request_tick_frequency(1); // force display since it normally won't update til midnight. @@ -152,7 +149,7 @@ bool day_one_face_loop(movement_event_t event, movement_settings_t *settings, vo switch (state->current_page) { case PAGE_YEAR: state->birth_year = state->birth_year + 1; - if (state->birth_year > state->current_year) state->birth_year = 1900; + if (state->birth_year > 2080) state->birth_year = 1900; break; case PAGE_MONTH: state->birth_month = (state->birth_month % 12) + 1; diff --git a/movement/watch_faces/complication/day_one_face.h b/movement/watch_faces/complication/day_one_face.h index 5d822a5e..c2a87c82 100644 --- a/movement/watch_faces/complication/day_one_face.h +++ b/movement/watch_faces/complication/day_one_face.h @@ -39,7 +39,6 @@ typedef enum { typedef struct { day_one_page_t current_page; - uint16_t current_year; uint16_t birth_year; uint8_t birth_month; uint8_t birth_day; From e8b7985dde2ef7a680429b8992e481d30b676b32 Mon Sep 17 00:00:00 2001 From: Christian Buschau Date: Tue, 19 Sep 2023 18:31:56 +0200 Subject: [PATCH 21/45] day_one_face: enable quick cycle through settings This allows the alarm button to be held down in the date settings and quickly cycle through the dates instead of having to push for each single increment like in other faces. --- .../watch_faces/complication/day_one_face.c | 66 +++++++++++++------ .../watch_faces/complication/day_one_face.h | 1 + 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/movement/watch_faces/complication/day_one_face.c b/movement/watch_faces/complication/day_one_face.c index c2c3d210..d0cf9f75 100644 --- a/movement/watch_faces/complication/day_one_face.c +++ b/movement/watch_faces/complication/day_one_face.c @@ -27,6 +27,8 @@ #include "day_one_face.h" #include "watch.h" +static const uint8_t days_in_month[12] = {31, 29, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31}; + static uint32_t _day_one_face_juliandaynum(uint16_t year, uint16_t month, uint16_t day) { // from here: https://en.wikipedia.org/wiki/Julian_day#Julian_day_number_calculation return (1461 * (year + 4800 + (month - 14) / 12)) / 4 + (367 * (month - 2 - 12 * ((month - 14) / 12))) / 12 - (3 * ((year + 4900 + (month - 14) / 12) / 100))/4 + day - 32075; @@ -45,6 +47,34 @@ static void _day_one_face_update(day_one_state_t *state) { watch_display_string(buf, 0); } +static void _day_one_face_abort_quick_cycle(day_one_state_t *state) { + if (state->quick_cycle) { + state->quick_cycle = false; + movement_request_tick_frequency(4); + } +} + +static void _day_one_face_increment(day_one_state_t *state) { + state->birthday_changed = true; + switch (state->current_page) { + case PAGE_YEAR: + state->birth_year = state->birth_year + 1; + if (state->birth_year > 2080) state->birth_year = 1900; + break; + case PAGE_MONTH: + state->birth_month = (state->birth_month % 12) + 1; + break; + case PAGE_DAY: + state->birth_day = state->birth_day + 1; + if (state->birth_day == 0 || state->birth_day > days_in_month[state->birth_month - 1]) { + state->birth_day = 1; + } + break; + default: + break; + } +} + void day_one_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { (void) settings; (void) watch_face_index; @@ -69,6 +99,7 @@ void day_one_face_activate(movement_settings_t *settings, void *context) { day_one_state_t *state = (day_one_state_t *)context; state->current_page = PAGE_DISPLAY; + state->quick_cycle = false; // fetch the user's birth date from the birthday register. movement_birthdate_t movement_birthdate = (movement_birthdate_t) watch_get_backup_data(2); @@ -81,7 +112,6 @@ bool day_one_face_loop(movement_event_t event, movement_settings_t *settings, vo (void) settings; day_one_state_t *state = (day_one_state_t *)context; - const uint8_t days_in_month[12] = {31, 29, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31}; char buf[6]; switch (event.event_type) { @@ -91,6 +121,13 @@ bool day_one_face_loop(movement_event_t event, movement_settings_t *settings, vo case EVENT_LOW_ENERGY_UPDATE: case EVENT_TICK: if (state->current_page != PAGE_DISPLAY) { + if (state->quick_cycle) { + if (watch_get_pin_level(BTN_ALARM)) { + _day_one_face_increment(state); + } else { + _day_one_face_abort_quick_cycle(state); + } + } // if in settings mode, update whatever the current page is switch (state->current_page) { case PAGE_YEAR: @@ -145,24 +182,8 @@ bool day_one_face_loop(movement_event_t event, movement_settings_t *settings, vo case EVENT_ALARM_BUTTON_UP: // if we are on a settings page, increment whatever value we're setting. if (state->current_page != PAGE_DISPLAY) { - state->birthday_changed = true; - switch (state->current_page) { - case PAGE_YEAR: - state->birth_year = state->birth_year + 1; - if (state->birth_year > 2080) state->birth_year = 1900; - break; - case PAGE_MONTH: - state->birth_month = (state->birth_month % 12) + 1; - break; - case PAGE_DAY: - state->birth_day = state->birth_day + 1; - if (state->birth_day == 0 || state->birth_day > days_in_month[state->birth_month - 1]) { - state->birth_day = 1; - } - break; - default: - break; - } + _day_one_face_abort_quick_cycle(state); + _day_one_face_increment(state); } break; case EVENT_ALARM_LONG_PRESS: @@ -170,9 +191,16 @@ bool day_one_face_loop(movement_event_t event, movement_settings_t *settings, vo if (state->current_page == PAGE_DISPLAY) { state->current_page++; movement_request_tick_frequency(4); + } else { + state->quick_cycle = true; + movement_request_tick_frequency(8); } break; + case EVENT_ALARM_LONG_UP: + _day_one_face_abort_quick_cycle(state); + break; case EVENT_TIMEOUT: + _day_one_face_abort_quick_cycle(state); // return home if we're on a settings page (this saves our changes when we resign). if (state->current_page != PAGE_DISPLAY) { movement_move_to_face(0); diff --git a/movement/watch_faces/complication/day_one_face.h b/movement/watch_faces/complication/day_one_face.h index c2a87c82..018bd09b 100644 --- a/movement/watch_faces/complication/day_one_face.h +++ b/movement/watch_faces/complication/day_one_face.h @@ -43,6 +43,7 @@ typedef struct { uint8_t birth_month; uint8_t birth_day; bool birthday_changed; + bool quick_cycle; } day_one_state_t; void day_one_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); From 984990fb6639c9b24f2edcc8ca50390e1ce3aa0c Mon Sep 17 00:00:00 2001 From: Christian Buschau Date: Wed, 20 Sep 2023 00:55:51 +0200 Subject: [PATCH 22/45] day_one_face: show set date on short alarm button press --- .../watch_faces/complication/day_one_face.c | 161 +++++++++++------- .../watch_faces/complication/day_one_face.h | 4 +- 2 files changed, 107 insertions(+), 58 deletions(-) diff --git a/movement/watch_faces/complication/day_one_face.c b/movement/watch_faces/complication/day_one_face.c index d0cf9f75..27601edc 100644 --- a/movement/watch_faces/complication/day_one_face.c +++ b/movement/watch_faces/complication/day_one_face.c @@ -100,6 +100,7 @@ void day_one_face_activate(movement_settings_t *settings, void *context) { state->current_page = PAGE_DISPLAY; state->quick_cycle = false; + state->ticks = 0; // fetch the user's birth date from the birthday register. movement_birthdate_t movement_birthdate = (movement_birthdate_t) watch_get_backup_data(2); @@ -112,7 +113,7 @@ bool day_one_face_loop(movement_event_t event, movement_settings_t *settings, vo (void) settings; day_one_state_t *state = (day_one_state_t *)context; - char buf[6]; + char buf[9]; switch (event.event_type) { case EVENT_ACTIVATE: @@ -120,80 +121,126 @@ bool day_one_face_loop(movement_event_t event, movement_settings_t *settings, vo break; case EVENT_LOW_ENERGY_UPDATE: case EVENT_TICK: - if (state->current_page != PAGE_DISPLAY) { - if (state->quick_cycle) { - if (watch_get_pin_level(BTN_ALARM)) { - _day_one_face_increment(state); - } else { - _day_one_face_abort_quick_cycle(state); - } + if (state->quick_cycle) { + if (watch_get_pin_level(BTN_ALARM)) { + _day_one_face_increment(state); + } else { + _day_one_face_abort_quick_cycle(state); } + } + switch (state->current_page) { // if in settings mode, update whatever the current page is - switch (state->current_page) { - case PAGE_YEAR: - watch_display_string("YR ", 0); - if (event.subsecond % 2) { - sprintf(buf, "%4d", state->birth_year); - watch_display_string(buf, 4); - } - break; - case PAGE_MONTH: - watch_display_string("MO ", 0); - if (event.subsecond % 2) { - sprintf(buf, "%2d", state->birth_month); - watch_display_string(buf, 4); - } - break; - case PAGE_DAY: - watch_display_string("DA ", 0); - if (event.subsecond % 2) { - sprintf(buf, "%2d", state->birth_day); - watch_display_string(buf, 6); - } - break; - default: - break; - } - } else { + case PAGE_YEAR: + watch_display_string("YR ", 0); + if (event.subsecond % 2) { + sprintf(buf, "%4d", state->birth_year); + watch_display_string(buf, 4); + } + break; + case PAGE_MONTH: + watch_display_string("MO ", 0); + if (event.subsecond % 2) { + sprintf(buf, "%2d", state->birth_month); + watch_display_string(buf, 4); + } + break; + case PAGE_DAY: + watch_display_string("DA ", 0); + if (event.subsecond % 2) { + sprintf(buf, "%2d", state->birth_day); + watch_display_string(buf, 6); + } + break; // otherwise, check if we have to update. the display only needs to change at midnight! - watch_date_time date_time = watch_rtc_get_date_time(); - if (date_time.unit.hour == 0 && date_time.unit.minute == 0 && date_time.unit.second == 0) { - _day_one_face_update(state); - } + case PAGE_DISPLAY: { + watch_date_time date_time = watch_rtc_get_date_time(); + if (date_time.unit.hour == 0 && date_time.unit.minute == 0 && date_time.unit.second == 0) { + _day_one_face_update(state); + } + break;} + case PAGE_DATE: + if (state->ticks > 0) { + state->ticks--; + } else { + state->current_page = PAGE_DISPLAY; + _day_one_face_update(state); + } + break; + default: + break; } break; case EVENT_LIGHT_BUTTON_DOWN: // only illuminate if we're in display mode - if (state->current_page == PAGE_DISPLAY) movement_illuminate_led(); + switch (state->current_page) { + case PAGE_DISPLAY: + // fall through + case PAGE_DATE: + movement_illuminate_led(); + break; + default: + break; + } break; case EVENT_LIGHT_BUTTON_UP: // otherwise use the light button to advance settings pages. - if (state->current_page != PAGE_DISPLAY) { - // go to next setting page... - state->current_page = (state->current_page + 1) % 4; - if (state->current_page == PAGE_DISPLAY) { - // ...unless we've been pushed back to display mode. - movement_request_tick_frequency(1); - // force display since it normally won't update til midnight. - _day_one_face_update(state); - } + switch (state->current_page) { + case PAGE_YEAR: + // fall through + case PAGE_MONTH: + // fall through + case PAGE_DAY: + // go to next setting page... + state->current_page = (state->current_page + 1) % 4; + if (state->current_page == PAGE_DISPLAY) { + // ...unless we've been pushed back to display mode. + movement_request_tick_frequency(1); + // force display since it normally won't update til midnight. + _day_one_face_update(state); + } + break; + default: + break; } break; case EVENT_ALARM_BUTTON_UP: // if we are on a settings page, increment whatever value we're setting. - if (state->current_page != PAGE_DISPLAY) { - _day_one_face_abort_quick_cycle(state); - _day_one_face_increment(state); + switch (state->current_page) { + case PAGE_YEAR: + // fall through + case PAGE_MONTH: + // fall through + case PAGE_DAY: + _day_one_face_abort_quick_cycle(state); + _day_one_face_increment(state); + break; + case PAGE_DISPLAY: + state->current_page = PAGE_DATE; + sprintf(buf, "%04d%02d%02d", state->birth_year % 10000, state->birth_month % 100, state->birth_day % 100); + watch_display_string(buf, 2); + state->ticks = 2; + break; + default: + break; } break; case EVENT_ALARM_LONG_PRESS: // if we aren't already in settings mode, put us there. - if (state->current_page == PAGE_DISPLAY) { - state->current_page++; - movement_request_tick_frequency(4); - } else { - state->quick_cycle = true; - movement_request_tick_frequency(8); + switch (state->current_page) { + case PAGE_DISPLAY: + state->current_page++; + movement_request_tick_frequency(4); + break; + case PAGE_YEAR: + // fall through + case PAGE_MONTH: + // fall through + case PAGE_DAY: + state->quick_cycle = true; + movement_request_tick_frequency(8); + break; + default: + break; } break; case EVENT_ALARM_LONG_UP: diff --git a/movement/watch_faces/complication/day_one_face.h b/movement/watch_faces/complication/day_one_face.h index 018bd09b..db1d05ed 100644 --- a/movement/watch_faces/complication/day_one_face.h +++ b/movement/watch_faces/complication/day_one_face.h @@ -34,7 +34,8 @@ typedef enum { PAGE_DISPLAY, PAGE_YEAR, PAGE_MONTH, - PAGE_DAY + PAGE_DAY, + PAGE_DATE } day_one_page_t; typedef struct { @@ -44,6 +45,7 @@ typedef struct { uint8_t birth_day; bool birthday_changed; bool quick_cycle; + uint8_t ticks; } day_one_state_t; void day_one_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); From 2e9ea8c36f1af70dfaa8a428a31afe961c48bbef Mon Sep 17 00:00:00 2001 From: Hugo Chargois Date: Sat, 18 Nov 2023 00:28:54 +0100 Subject: [PATCH 23/45] Improve simulator page design --- watch-library/simulator/shell.html | 66 ++++++++++++++++++------------ 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/watch-library/simulator/shell.html b/watch-library/simulator/shell.html index 583829cc..c8da063e 100644 --- a/watch-library/simulator/shell.html +++ b/watch-library/simulator/shell.html @@ -37,12 +37,13 @@ .highlight { fill: #fff !important; } #skinselect label { display: inline-block; - padding: 8px; + padding: 4px; background-color: black; color: white; border-radius: 8px; border: 2px solid #0e57a9; outline: 4px solid black; + margin: 4px; cursor: pointer; } #skinselect #a158wea-label { @@ -50,13 +51,16 @@ color: black; border-color: black; outline-color: #b68855ff; - + } + h2 { + margin: 8px 0; + font-size: 1em; } -
+

Sensor Watch Emulator

@@ -882,30 +886,40 @@ - - - - - - -
- - - - Original F-91W SVG is © 2020 Alexis Philip,
used here under the terms of the MIT license. -
- -
-
- -
- - -
- +
+

Skin

+
+ + +
+ +

Volume

+
+ +
+ +

Location

+
+ +
+
+ +
+ +
+ + +
+
+ +

+ Original F-91W SVG is © 2020 Alexis Philip, used here + under the terms of the MIT license. +

+