diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b150afb..6b4fc79 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: diff --git a/make.mk b/make.mk index bf61708..bb2d153 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 @@ -120,6 +121,7 @@ SRCS += \ $(TOP)/watch-library/hardware/watch/watch_storage.c \ $(TOP)/watch-library/hardware/watch/watch_deepsleep.c \ $(TOP)/watch-library/hardware/watch/watch_private.c \ + $(TOP)/watch-library/hardware/watch/watch_private_cdc.c \ $(TOP)/watch-library/hardware/watch/watch.c \ $(TOP)/watch-library/hardware/hal/src/hal_atomic.c \ $(TOP)/watch-library/hardware/hal/src/hal_delay.c \ @@ -207,7 +209,15 @@ ifeq ($(LED), BLUE) CFLAGS += -DWATCH_IS_BLUE_BOARD endif -ifeq ($(LED), RED) +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 + +ifeq ($(COLOR), RED) CFLAGS += -DWATCH_INVERT_LED_POLARITY CFLAGS += -DNO_FREQCORR endif @@ -220,3 +230,9 @@ endif ifeq ($(BOARD), OSO-FEAL-A1-00) CFLAGS += -DCRYSTALLESS endif + +# Build options to customize movement and faces + +ifdef CLOCK_FACE_24H_ONLY +CFLAGS += -DCLOCK_FACE_24H_ONLY +endif diff --git a/movement/alt_fw/alt_time.h b/movement/alt_fw/alt_time.h index 8108bcf..956928d 100644 --- a/movement/alt_fw/alt_time.h +++ b/movement/alt_fw/alt_time.h @@ -38,4 +38,6 @@ const watch_face_t watch_faces[] = { #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) +#define SIGNAL_TUNE_DEFAULT + #endif // MOVEMENT_CONFIG_H_ diff --git a/movement/alt_fw/backer.h b/movement/alt_fw/backer.h index 3abcf45..d41f567 100644 --- a/movement/alt_fw/backer.h +++ b/movement/alt_fw/backer.h @@ -40,4 +40,6 @@ const watch_face_t watch_faces[] = { #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) +#define SIGNAL_TUNE_DEFAULT + #endif // MOVEMENT_CONFIG_H_ diff --git a/movement/alt_fw/deep_space_now.h b/movement/alt_fw/deep_space_now.h index 6cb3423..d2096a6 100644 --- a/movement/alt_fw/deep_space_now.h +++ b/movement/alt_fw/deep_space_now.h @@ -58,4 +58,6 @@ const watch_face_t watch_faces[] = { #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) +#define SIGNAL_TUNE_DEFAULT + #endif // MOVEMENT_CONFIG_H_ diff --git a/movement/alt_fw/focus.h b/movement/alt_fw/focus.h index ab5525a..a8fd573 100644 --- a/movement/alt_fw/focus.h +++ b/movement/alt_fw/focus.h @@ -40,4 +40,6 @@ const watch_face_t watch_faces[] = { #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) +#define SIGNAL_TUNE_DEFAULT + #endif // MOVEMENT_CONFIG_H_ diff --git a/movement/alt_fw/the_athlete.h b/movement/alt_fw/the_athlete.h index 5d9d952..660a304 100644 --- a/movement/alt_fw/the_athlete.h +++ b/movement/alt_fw/the_athlete.h @@ -40,4 +40,6 @@ const watch_face_t watch_faces[] = { #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) +#define SIGNAL_TUNE_DEFAULT + #endif // MOVEMENT_CONFIG_H_ diff --git a/movement/alt_fw/the_backpacker.h b/movement/alt_fw/the_backpacker.h index 01af7b0..0a599cf 100644 --- a/movement/alt_fw/the_backpacker.h +++ b/movement/alt_fw/the_backpacker.h @@ -41,4 +41,6 @@ const watch_face_t watch_faces[] = { #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) +#define SIGNAL_TUNE_DEFAULT + #endif // MOVEMENT_CONFIG_H_ diff --git a/movement/alt_fw/the_stargazer.h b/movement/alt_fw/the_stargazer.h index a13dc3a..844c680 100644 --- a/movement/alt_fw/the_stargazer.h +++ b/movement/alt_fw/the_stargazer.h @@ -42,4 +42,6 @@ const watch_face_t watch_faces[] = { #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) +#define SIGNAL_TUNE_DEFAULT + #endif // MOVEMENT_CONFIG_H_ diff --git a/movement/alt_fw/timers.h b/movement/alt_fw/timers.h index c4d27f5..4d36c66 100644 --- a/movement/alt_fw/timers.h +++ b/movement/alt_fw/timers.h @@ -40,4 +40,6 @@ const watch_face_t watch_faces[] = { #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) +#define SIGNAL_TUNE_DEFAULT + #endif // MOVEMENT_CONFIG_H_ diff --git a/movement/filesystem.c b/movement/filesystem.c index 97e3545..9df0a8d 100644 --- a/movement/filesystem.c +++ b/movement/filesystem.c @@ -100,7 +100,7 @@ static int filesystem_ls(lfs_t *lfs, const char *path) { printf("%4ld bytes ", info.size); - printf("%s\n", info.name); + printf("%s\r\n", info.name); } err = lfs_dir_close(lfs, &dir); @@ -117,11 +117,11 @@ bool filesystem_init(void) { // reformat if we can't mount the filesystem // this should only happen on the first boot if (err < 0) { - printf("Ignore that error! Formatting filesystem...\n"); + printf("Ignore that error! Formatting filesystem...\r\n"); err = lfs_format(&lfs, &cfg); if (err < 0) return false; err = lfs_mount(&lfs, &cfg) == LFS_ERR_OK; - printf("Filesystem mounted with %ld bytes free.\n", filesystem_get_free_space()); + printf("Filesystem mounted with %ld bytes free.\r\n", filesystem_get_free_space()); } return err == LFS_ERR_OK; @@ -139,7 +139,7 @@ bool filesystem_rm(char *filename) { if (filesystem_file_exists(filename)) { return lfs_remove(&lfs, filename) == LFS_ERR_OK; } else { - printf("rm: %s: No such file\n", filename); + printf("rm: %s: No such file\r\n", filename); return false; } } @@ -197,13 +197,13 @@ static void filesystem_cat(char *filename) { char *buf = malloc(info.size + 1); filesystem_read_file(filename, buf, info.size); buf[info.size] = '\0'; - printf("%s\n", buf); + printf("%s\r\n", buf); free(buf); } else { - printf("\n"); + printf("\r\n"); } } else { - printf("cat: %s: No such file\n", filename); + printf("cat: %s: No such file\r\n", filename); } } @@ -223,59 +223,60 @@ bool filesystem_append_file(char *filename, char *text, int32_t length) { return lfs_file_close(&lfs, &file) == LFS_ERR_OK; } -void filesystem_process_command(char *line) { - printf("$ %s", line); - char *command = strtok(line, " \n"); - - if (strcmp(command, "ls") == 0) { - char *directory = strtok(NULL, " \n"); - if (directory == NULL) { - filesystem_ls(&lfs, "/"); - } else { - printf("usage: ls\n"); - } - } else if (strcmp(command, "cat") == 0) { - char *filename = strtok(NULL, " \n"); - if (filename == NULL) { - printf("usage: cat file\n"); - } else { - filesystem_cat(filename); - } - } else if (strcmp(command, "df") == 0) { - printf("free space: %ld bytes\n", filesystem_get_free_space()); - } else if (strcmp(command, "rm") == 0) { - char *filename = strtok(NULL, " \n"); - if (filename == NULL) { - printf("usage: rm file\n"); - } else { - filesystem_rm(filename); - } - } else if (strcmp(command, "echo") == 0) { - char *text = malloc(248); - memset(text, 0, 248); - size_t pos = 0; - char *word = strtok(NULL, " \n"); - while (strcmp(word, ">") && strcmp(word, ">>")) { - sprintf(text + pos, "%s ", word); - pos += strlen(word) + 1; - word = strtok(NULL, " \n"); - if (word == NULL) break; - } - text[strlen(text) - 1] = 0; - char *filename = strtok(NULL, " \n"); - if (filename == NULL) { - printf("usage: echo text > file\n"); - } else if (strchr(filename, '/') || strchr(filename, '\\')) { - printf("subdirectories are not supported\n"); - } else if (!strcmp(word, ">")) { - filesystem_write_file(filename, text, strlen(text)); - filesystem_append_file(filename, "\n", 1); - } else if (!strcmp(word, ">>")) { - filesystem_append_file(filename, text, strlen(text)); - filesystem_append_file(filename, "\n", 1); - } - free(text); +int filesystem_cmd_ls(int argc, char *argv[]) { + if (argc >= 2) { + filesystem_ls(&lfs, argv[1]); } else { - printf("%s: command not found\n", command); + filesystem_ls(&lfs, "/"); } + return 0; } + +int filesystem_cmd_cat(int argc, char *argv[]) { + (void) argc; + filesystem_cat(argv[1]); + return 0; +} + +int filesystem_cmd_df(int argc, char *argv[]) { + (void) argc; + (void) argv; + printf("free space: %ld bytes\r\n", filesystem_get_free_space()); + return 0; +} + +int filesystem_cmd_rm(int argc, char *argv[]) { + (void) argc; + filesystem_rm(argv[1]); + return 0; +} + +int filesystem_cmd_echo(int argc, char *argv[]) { + (void) argc; + + char *line = argv[1]; + size_t line_len = strlen(line); + if (line[0] == '"' || line[0] == '\'') { + line++; + line_len -= 2; + line[line_len] = '\0'; + } + + if (strchr(argv[3], '/')) { + printf("subdirectories are not supported\r\n"); + return -2; + } + + if (!strcmp(argv[2], ">")) { + filesystem_write_file(argv[3], line, line_len); + filesystem_append_file(argv[3], "\n", 1); + } else if (!strcmp(argv[2], ">>")) { + filesystem_append_file(argv[3], line, line_len); + filesystem_append_file(argv[3], "\n", 1); + } else { + return -2; + } + + return 0; +} + diff --git a/movement/filesystem.h b/movement/filesystem.h index 3cd3d09..fa3d9d1 100644 --- a/movement/filesystem.h +++ b/movement/filesystem.h @@ -96,9 +96,10 @@ bool filesystem_write_file(char *filename, char *text, int32_t length); */ bool filesystem_append_file(char *filename, char *text, int32_t length); -/** @brief Handles the interactive file browser when Movement is plugged in to USB. - * @param line The command that the user typed into the serial console. - */ -void filesystem_process_command(char *line); +int filesystem_cmd_ls(int argc, char *argv[]); +int filesystem_cmd_cat(int argc, char *argv[]); +int filesystem_cmd_df(int argc, char *argv[]); +int filesystem_cmd_rm(int argc, char *argv[]); +int filesystem_cmd_echo(int argc, char *argv[]); #endif // FILESYSTEM_H_ diff --git a/movement/make/Makefile b/movement/make/Makefile index 625c772..da5486b 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -49,7 +49,10 @@ SRCS += \ ../../littlefs/lfs_util.c \ ../movement.c \ ../filesystem.c \ + ../shell.c \ + ../shell_cmd_list.c \ ../watch_faces/clock/simple_clock_face.c \ + ../watch_faces/clock/clock_face.c \ ../watch_faces/clock/world_clock_face.c \ ../watch_faces/clock/beats_face.c \ ../watch_faces/clock/weeknumber_clock_face.c \ @@ -118,6 +121,14 @@ SRCS += \ ../watch_faces/complication/flashlight_face.c \ ../watch_faces/clock/decimal_time_face.c \ ../watch_faces/clock/wyoscan_face.c \ + ../watch_faces/settings/save_load_face.c \ + ../watch_faces/clock/day_night_percentage_face.c \ + ../watch_faces/complication/simple_coin_flip_face.c \ + ../watch_faces/complication/solstice_face.c \ + ../watch_faces/complication/couch_to_5k_face.c \ + ../watch_faces/clock/minute_repeater_decimal_face.c \ + ../watch_faces/complication/tuning_tones_face.c \ + ../watch_faces/complication/kitchen_conversions_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/make/make_alternate_fw.sh b/movement/make/make_alternate_fw.sh index d1ce767..df27403 100755 --- a/movement/make/make_alternate_fw.sh +++ b/movement/make/make_alternate_fw.sh @@ -2,7 +2,7 @@ fw_dir="firmware/download" sim_dir="firmware/simulate" -colors=("green" "blue") +colors=("green" "blue" "red") variants=("standard" "backer" "alt_time" "deep_space_now" "focus" "the_athlete" "the_backpacker" "the_stargazer") if [ -d "$fw_dir" ] ; then @@ -21,12 +21,12 @@ do for color in "${colors[@]}" do COLOR=$(echo "$color" | tr '[:lower:]' '[:upper:]') - make clean - make LED=$COLOR FIRMWARE=$VARIANT + make COLOR=$COLOR clean + make COLOR=$COLOR FIRMWARE=$VARIANT mv "build/watch.uf2" "$fw_dir/$variant-$color.uf2" done rm -rf ./build-sim - emmake make FIRMWARE=$VARIANT + emmake make COLOR=GREEN FIRMWARE=$VARIANT mkdir "$sim_dir/$variant/" mv "build-sim/watch.wasm" "$sim_dir/$variant/" mv "build-sim/watch.js" "$sim_dir/$variant/" diff --git a/movement/movement.c b/movement/movement.c index cf2eb27..8b45535 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -33,6 +33,7 @@ #include "watch.h" #include "filesystem.h" #include "movement.h" +#include "shell.h" #ifndef MOVEMENT_FIRMWARE #include "movement_config.h" @@ -54,6 +55,8 @@ #include "alt_fw/deep_space_now.h" #endif +#include "movement_custom_signal_tunes.h" + // Default to no secondary face behaviour. #ifndef MOVEMENT_SECONDARY_FACE_INDEX #define MOVEMENT_SECONDARY_FACE_INDEX 0 @@ -67,6 +70,31 @@ #define MOVEMENT_DEFAULT_GREEN_COLOR 0xF #endif +// Default to 12h mode +#ifndef MOVEMENT_DEFAULT_24H_MODE +#define MOVEMENT_DEFAULT_24H_MODE false +#endif + +// Default to mode button sounding on press +#ifndef MOVEMENT_DEFAULT_BUTTON_SOUND +#define MOVEMENT_DEFAULT_BUTTON_SOUND true +#endif + +// Default to switch back to main watch face after 60 seconds +#ifndef MOVEMENT_DEFAULT_TIMEOUT_INTERVAL +#define MOVEMENT_DEFAULT_TIMEOUT_INTERVAL 0 +#endif + +// Default to switch to low energy mode after 2 hours +#ifndef MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL +#define MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL 2 +#endif + +// Default to 1 second led duration +#ifndef MOVEMENT_DEFAULT_LED_DURATION +#define MOVEMENT_DEFAULT_LED_DURATION 1 +#endif + #if __EMSCRIPTEN__ #include #endif @@ -74,7 +102,7 @@ movement_state_t movement_state; void * watch_face_contexts[MOVEMENT_NUM_FACES]; watch_date_time scheduled_tasks[MOVEMENT_NUM_FACES]; -const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 3600, 7200, 21600, 43200, 86400, 172800, 604800}; +const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800}; const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800}; movement_event_t event; @@ -331,7 +359,7 @@ bool movement_default_loop_handler(movement_event_t event, movement_settings_t * movement_illuminate_led(); break; case EVENT_MODE_LONG_PRESS: - if (MOVEMENT_SECONDARY_FACE_INDEX && movement_state.current_watch_face == 0) { + if (MOVEMENT_SECONDARY_FACE_INDEX && movement_state.current_face_idx == 0) { movement_move_to_face(MOVEMENT_SECONDARY_FACE_INDEX); } else { movement_move_to_face(0); @@ -346,25 +374,25 @@ bool movement_default_loop_handler(movement_event_t event, movement_settings_t * void movement_move_to_face(uint8_t watch_face_index) { movement_state.watch_face_changed = true; - movement_state.next_watch_face = watch_face_index; + movement_state.next_face_idx = watch_face_index; } void movement_move_to_next_face(void) { uint16_t face_max; if (MOVEMENT_SECONDARY_FACE_INDEX) { - face_max = (movement_state.current_watch_face < (int16_t)MOVEMENT_SECONDARY_FACE_INDEX) ? MOVEMENT_SECONDARY_FACE_INDEX : MOVEMENT_NUM_FACES; + face_max = (movement_state.current_face_idx < (int16_t)MOVEMENT_SECONDARY_FACE_INDEX) ? MOVEMENT_SECONDARY_FACE_INDEX : MOVEMENT_NUM_FACES; } else { face_max = MOVEMENT_NUM_FACES; } - movement_move_to_face((movement_state.current_watch_face + 1) % face_max); + movement_move_to_face((movement_state.current_face_idx + 1) % face_max); } void movement_schedule_background_task(watch_date_time date_time) { - movement_schedule_background_task_for_face(movement_state.current_watch_face, date_time); + movement_schedule_background_task_for_face(movement_state.current_face_idx, date_time); } void movement_cancel_background_task(void) { - movement_cancel_background_task_for_face(movement_state.current_watch_face); + movement_cancel_background_task_for_face(movement_state.current_face_idx); } void movement_schedule_background_task_for_face(uint8_t watch_face_index, watch_date_time date_time) { @@ -392,8 +420,32 @@ void movement_request_wake() { _movement_reset_inactivity_countdown(); } +static void end_buzzing() { + movement_state.is_buzzing = false; +} + +static void end_buzzing_and_disable_buzzer(void) { + end_buzzing(); + watch_disable_buzzer(); +} + void movement_play_signal(void) { - watch_buzzer_play_sequence(signal_tune, NULL); + void *maybe_disable_buzzer = end_buzzing_and_disable_buzzer; + if (watch_is_buzzer_or_led_enabled()) { + maybe_disable_buzzer = end_buzzing; + } else { + watch_enable_buzzer(); + } + movement_state.is_buzzing = true; + watch_buzzer_play_sequence(signal_tune, maybe_disable_buzzer); + if (movement_state.le_mode_ticks == -1) { + // the watch is asleep. wake it up for "1" round through the main loop. + // the sleep_mode_app_loop will notice the is_buzzing and note that it + // only woke up to beep and then it will spinlock until the callback + // turns off the is_buzzing flag. + movement_state.needs_wake = true; + movement_state.le_mode_ticks = 1; + } } void movement_play_alarm(void) { @@ -426,11 +478,13 @@ void app_init(void) { memset(&movement_state, 0, sizeof(movement_state)); + movement_state.settings.bit.clock_mode_24h = MOVEMENT_DEFAULT_24H_MODE; movement_state.settings.bit.led_red_color = MOVEMENT_DEFAULT_RED_COLOR; movement_state.settings.bit.led_green_color = MOVEMENT_DEFAULT_GREEN_COLOR; - movement_state.settings.bit.button_should_sound = true; - movement_state.settings.bit.le_interval = 1; - movement_state.settings.bit.led_duration = 1; + movement_state.settings.bit.button_should_sound = MOVEMENT_DEFAULT_BUTTON_SOUND; + movement_state.settings.bit.to_interval = MOVEMENT_DEFAULT_TIMEOUT_INTERVAL; + movement_state.settings.bit.le_interval = MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL; + movement_state.settings.bit.led_duration = MOVEMENT_DEFAULT_LED_DURATION; movement_state.light_ticks = -1; movement_state.alarm_ticks = -1; movement_state.next_available_backup_register = 4; @@ -495,7 +549,7 @@ void app_setup(void) { watch_faces[i].setup(&movement_state.settings, i, &watch_face_contexts[i]); } - watch_faces[movement_state.current_watch_face].activate(&movement_state.settings, watch_face_contexts[movement_state.current_watch_face]); + watch_faces[movement_state.current_face_idx].activate(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]); event.subsecond = 0; event.event_type = EVENT_ACTIVATE; } @@ -515,7 +569,7 @@ static void _sleep_mode_app_loop(void) { if (movement_state.needs_background_tasks_handled) _movement_handle_background_tasks(); event.event_type = EVENT_LOW_ENERGY_UPDATE; - watch_faces[movement_state.current_watch_face].loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_watch_face]); + watch_faces[movement_state.current_face_idx].loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]); // if we need to wake immediately, do it! if (movement_state.needs_wake) return; @@ -525,16 +579,20 @@ static void _sleep_mode_app_loop(void) { } bool app_loop(void) { + const watch_face_t *wf = &watch_faces[movement_state.current_face_idx]; + bool woke_up_for_buzzer = false; if (movement_state.watch_face_changed) { if (movement_state.settings.bit.button_should_sound) { // low note for nonzero case, high note for return to watch_face 0 - watch_buzzer_play_note(movement_state.next_watch_face ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50); + watch_buzzer_play_note(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50); } - watch_faces[movement_state.current_watch_face].resign(&movement_state.settings, watch_face_contexts[movement_state.current_watch_face]); - movement_state.current_watch_face = movement_state.next_watch_face; + wf->resign(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]); + movement_state.current_face_idx = movement_state.next_face_idx; + // we have just updated the face idx, so we must recache the watch face pointer. + wf = &watch_faces[movement_state.current_face_idx]; watch_clear_display(); movement_request_tick_frequency(1); - watch_faces[movement_state.current_watch_face].activate(&movement_state.settings, watch_face_contexts[movement_state.current_watch_face]); + wf->activate(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]); event.subsecond = 0; event.event_type = EVENT_ACTIVATE; movement_state.watch_face_changed = false; @@ -568,18 +626,24 @@ bool app_loop(void) { // _sleep_mode_app_loop takes over at this point and loops until le_mode_ticks is reset by the extwake handler, // or wake is requested using the movement_request_wake function. _sleep_mode_app_loop(); - // as soon as _sleep_mode_app_loop returns, we reactivate ourselves. + // as soon as _sleep_mode_app_loop returns, we prepare to reactivate + // ourselves, but first, we check to see if we woke up for the buzzer: + if (movement_state.is_buzzing) { + woke_up_for_buzzer = true; + } event.event_type = EVENT_ACTIVATE; // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks. // need to figure out if there's a better heuristic for determining how we woke up. app_setup(); } - static bool can_sleep = true; + // default to being allowed to sleep by the face. + bool can_sleep = true; if (event.event_type) { event.subsecond = movement_state.subsecond; - can_sleep = watch_faces[movement_state.current_watch_face].loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_watch_face]); + // the first trip through the loop overrides the can_sleep state + can_sleep = wf->loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]); event.event_type = EVENT_NONE; } @@ -591,9 +655,17 @@ bool app_loop(void) { event.event_type = EVENT_TIMEOUT; } event.subsecond = movement_state.subsecond; - watch_faces[movement_state.current_watch_face].loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_watch_face]); + // if we run through the loop again to time out, we need to reconsider whether or not we can sleep. + // if the first trip said true, but this trip said false, we need the false to override, thus + // we will be using boolean AND: + // + // first trip | can sleep | cannot sleep | can sleep | cannot sleep + // second trip | can sleep | cannot sleep | cannot sleep | can sleep + // && | can sleep | cannot sleep | cannot sleep | cannot sleep + bool can_sleep2 = wf->loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]); + can_sleep = can_sleep && can_sleep2; event.event_type = EVENT_NONE; - if (movement_state.settings.bit.to_always && movement_state.current_watch_face != 0) { + if (movement_state.settings.bit.to_always && movement_state.current_face_idx != 0) { // ...but if the user has "timeout always" set, give it the boot. movement_move_to_face(0); } @@ -619,30 +691,9 @@ bool app_loop(void) { } } - // if we are plugged into USB, handle the file browser tasks + // if we are plugged into USB, handle the serial shell if (watch_is_usb_enabled()) { - char line[256] = {0}; -#if __EMSCRIPTEN__ - // This is a terrible hack; ideally this should be handled deeper in the watch library. - // Alas, emscripten treats read() as something that should pop up an input box, so I - // wasn't able to implement this over there. I sense that this relates to read() being - // the wrong way to read data from USB (like we should be using fgets or something), but - // until I untangle that, this will have to do. - char *received_data = (char*)EM_ASM_INT({ - var len = lengthBytesUTF8(tx) + 1; - var s = _malloc(len); - stringToUTF8(tx, s, len); - return s; - }); - memcpy(line, received_data, min(255, strlen(received_data))); - free(received_data); - EM_ASM({ - tx = ""; - }); -#else - read(0, line, 256); -#endif - if (strlen(line)) filesystem_process_command(line); + shell_task(); } event.subsecond = 0; @@ -650,8 +701,13 @@ bool app_loop(void) { // if the watch face changed, we can't sleep because we need to update the display. if (movement_state.watch_face_changed) can_sleep = false; - // if the buzzer or the LED is on, we need to stay awake to keep the TCC running. - if (movement_state.is_buzzing || movement_state.light_ticks != -1) can_sleep = false; + // if we woke up for the buzzer, stay awake until it's finished. + if (woke_up_for_buzzer) { + while(watch_is_buzzer_or_led_enabled()); + } + + // if the LED is on, we need to stay awake to keep the TCC running. + if (movement_state.light_ticks != -1) can_sleep = false; return can_sleep; } diff --git a/movement/movement.h b/movement/movement.h index d61d510..d19ab3f 100644 --- a/movement/movement.h +++ b/movement/movement.h @@ -247,8 +247,8 @@ typedef struct { movement_settings_t settings; // transient properties - int16_t current_watch_face; - int16_t next_watch_face; + int16_t current_face_idx; + int16_t next_face_idx; bool watch_face_changed; bool fast_tick_enabled; int16_t fast_ticks; diff --git a/movement/movement_config.h b/movement/movement_config.h index 1bd1513..10a30af 100644 --- a/movement/movement_config.h +++ b/movement/movement_config.h @@ -49,8 +49,50 @@ const watch_face_t watch_faces[] = { */ #define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 2) // or (0) -/* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options */ +/* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */ #define SIGNAL_TUNE_DEFAULT -#include "movement_custom_signal_tunes.h" + +/* Determines the intensity of the led colors + * Set a hex value 0-15 with 0x0 being off and 0xF being max intensity + */ +#define MOVEMENT_DEFAULT_GREEN_COLOR 0xF +#define MOVEMENT_DEFAULT_RED_COLOR 0x0 + +/* Set to true for 24h mode or false for 12h mode */ +#define MOVEMENT_DEFAULT_24H_MODE false + +/* Enable or disable the sound on mode button press */ +#define MOVEMENT_DEFAULT_BUTTON_SOUND true + +/* Set the timeout before switching back to the main watch face + * Valid values are: + * 0: 60 seconds + * 1: 2 minutes + * 2: 5 minutes + * 3: 30 minutes + */ +#define MOVEMENT_DEFAULT_TIMEOUT_INTERVAL 0 + +/* Set the timeout before switching to low energy mode + * Valid values are: + * 0: Never + * 1: 1 hour + * 2: 2 hours + * 3: 6 hours + * 4: 12 hours + * 5: 1 day + * 6: 2 days + * 7: 7 days + */ +#define MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL 1 + +/* Set the led duration + * Valid values are: + * 0: No LED + * 1: 1 second + * 2: 3 seconds + * 3: 5 seconds + */ +#define MOVEMENT_DEFAULT_LED_DURATION 1 #endif // MOVEMENT_CONFIG_H_ diff --git a/movement/movement_faces.h b/movement/movement_faces.h index ff34c06..3557110 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -26,6 +26,7 @@ #define MOVEMENT_FACES_H_ #include "simple_clock_face.h" +#include "clock_face.h" #include "world_clock_face.h" #include "preferences_face.h" #include "set_time_face.h" @@ -95,6 +96,14 @@ #include "flashlight_face.h" #include "decimal_time_face.h" #include "wyoscan_face.h" +#include "save_load_face.h" +#include "day_night_percentage_face.h" +#include "simple_coin_flip_face.h" +#include "solstice_face.h" +#include "couch_to_5k_face.h" +#include "minute_repeater_decimal_face.h" +#include "tuning_tones_face.h" +#include "kitchen_conversions_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/shell.c b/movement/shell.c new file mode 100644 index 0000000..3782068 --- /dev/null +++ b/movement/shell.c @@ -0,0 +1,221 @@ +/* + * MIT License + * + * Copyright (c) 2023 Edward Shin + * + * 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 "shell.h" + +#include +#include +#include +#include +#include +#include +#include + +#if __EMSCRIPTEN__ +#include +#endif + +#include "watch.h" +#include "shell_cmd_list.h" + +extern shell_command_t g_shell_commands[]; +extern const size_t g_num_shell_commands; + +#define NEWLINE "\r\n" + +#define SHELL_BUF_SZ (256) +#define SHELL_MAX_ARGS (16) +#define SHELL_PROMPT "swsh> " + +static char s_buf[SHELL_BUF_SZ] = {0}; +static size_t s_buf_len = 0; +// Pointer to the first invalid byte after the end of input. +static char *const s_buf_end = s_buf + SHELL_BUF_SZ; + +static char *prv_skip_whitespace(char *c) { + while (c >= s_buf && c < s_buf_end) { + if (*c == 0) { + return NULL; + } + if ((!isspace((int) *c)) != 0) { + return c; + } + c++; + } + return NULL; +} + +static char *prv_skip_non_whitespace(char *c) { + bool in_quote = false; + char quote_char; + while (c >= s_buf && c < s_buf_end) { + if (*c == 0) { + return NULL; + } + // Basic handling of quoted arguments. + // Can't handle recursive quotes. :( + if (in_quote || *c == '"' || *c == '\'') { + if (!in_quote) { + quote_char = *c; + in_quote = true; + } else if (*c == quote_char) { + in_quote = false; + } + } else { + if (isspace((int) *c) != 0) { + return c; + } + } + c++; + } + return NULL; +} + +static int prv_handle_command() { + char *argv[SHELL_MAX_ARGS] = {0}; + int argc = 0; + + char *c = &s_buf[0]; + s_buf[SHELL_BUF_SZ - 1] = '\0'; + + while (argc < SHELL_MAX_ARGS) { + // Skip contiguous whitespace + c = prv_skip_whitespace(c); + if (c == NULL) { + // Reached end of buffer + break; + } + + // We hit non-whitespace, set argv and argc for this upcoming argument + argv[argc++] = c; + + // Skip contiguous non-whitespace + c = prv_skip_non_whitespace(c); + if (c == NULL) { + // Reached end of buffer + break; + } + + // NULL-terminate this arg string and then increment. + *(c++) = '\0'; + } + + if (argc == 0) { + return -1; + } + + // Match against the command list + for (size_t i = 0; i < g_num_shell_commands; i++) { + if (!strcasecmp(g_shell_commands[i].name, argv[0])) { + // If argc isn't valid for this command, display its help instead. + if (((argc - 1) < g_shell_commands[i].min_args) || + ((argc - 1) > g_shell_commands[i].max_args)) { + if (g_shell_commands[i].help != NULL) { + printf(NEWLINE "%s" NEWLINE, g_shell_commands[i].help); + } + return -2; + } + // Call the command's callback + if (g_shell_commands[i].cb != NULL) { + printf(NEWLINE); + int ret = g_shell_commands[i].cb(argc, argv); + if (ret == -2) { + printf(NEWLINE "%s" NEWLINE, g_shell_commands[i].help); + } + return ret; + } + } + } + + return -1; +} + +void shell_task(void) { +#if __EMSCRIPTEN__ + // This is a terrible hack; ideally this should be handled deeper in the watch library. + // Alas, emscripten treats read() as something that should pop up an input box, so I + // wasn't able to implement this over there. I sense that this relates to read() being + // the wrong way to read data from USB (like we should be using fgets or something), but + // until I untangle that, this will have to do. + char *received_data = (char*)EM_ASM_INT({ + var len = lengthBytesUTF8(tx) + 1; + var s = _malloc(len); + stringToUTF8(tx, s, len); + return s; + }); + s_buf_len = min((SHELL_BUF_SZ - 2), strlen(received_data)); + memcpy(s_buf, received_data, s_buf_len); + free(received_data); + s_buf[s_buf_len++] = '\n'; + s_buf[s_buf_len++] = '\0'; + prv_handle_command(); + EM_ASM({ + tx = ""; + }); +#else + // Read one character at a time until we run out. + while (true) { + if (s_buf_len >= (SHELL_BUF_SZ - 1)) { + printf(NEWLINE "Command too long, clearing."); + printf(NEWLINE SHELL_PROMPT); + s_buf_len = 0; + break; + } + + int c = getchar(); + + if (c < 0) { + // Nothing left to read, we're done. + break; + } + + if (c == '\b') { + // Handle backspace character. + // We need to emit a backspace, overwrite the character on the + // screen with a space, and then backspace again to move the cursor. + if (s_buf_len > 0) { + printf("\b \b"); + s_buf_len--; + } + continue; + } else if (c != '\n' && c != '\r') { + // Print regular characters to the screen. + putchar(c); + } + + s_buf[s_buf_len] = c; + + if (c == '\n' || c == '\r') { + // Newline! Handle the command. + s_buf[s_buf_len+1] = '\0'; + (void) prv_handle_command(); + s_buf_len = 0; + printf(NEWLINE SHELL_PROMPT); + break; + } else { + s_buf_len++; + } + } +#endif +} diff --git a/movement/shell.h b/movement/shell.h new file mode 100644 index 0000000..27dbf67 --- /dev/null +++ b/movement/shell.h @@ -0,0 +1,34 @@ +/* + * MIT License + * + * Copyright (c) 2023 Edward Shin + * + * 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 SHELL_H_ +#define SHELL_H_ + +/** @brief Called periodically from the app loop to handle shell commands. + * When a full command is complete, parses and executes its matching + * callback. + */ +void shell_task(void); + +#endif diff --git a/movement/shell_cmd_list.c b/movement/shell_cmd_list.c new file mode 100644 index 0000000..0ea08a5 --- /dev/null +++ b/movement/shell_cmd_list.c @@ -0,0 +1,159 @@ +/* + * MIT License + * + * Copyright (c) 2023 Edward Shin + * + * 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 "shell_cmd_list.h" + +#include +#include +#include + +#include "filesystem.h" +#include "watch.h" + +static int help_cmd(int argc, char *argv[]); +static int flash_cmd(int argc, char *argv[]); +static int stress_cmd(int argc, char *argv[]); + +shell_command_t g_shell_commands[] = { + { + .name = "?", + .help = "print command list", + .min_args = 0, + .max_args = 0, + .cb = help_cmd, + }, + { + .name = "help", + .help = "print command list", + .min_args = 0, + .max_args = 0, + .cb = help_cmd, + }, + { + .name = "flash", + .help = "reboot to UF2 bootloader", + .min_args = 0, + .max_args = 0, + .cb = flash_cmd, + }, + { + .name = "ls", + .help = "usage: ls [PATH]", + .min_args = 0, + .max_args = 1, + .cb = filesystem_cmd_ls, + }, + { + .name = "cat", + .help = "usage: cat ", + .min_args = 1, + .max_args = 1, + .cb = filesystem_cmd_cat, + }, + { + .name = "df", + .help = "print filesystem free space", + .min_args = 0, + .max_args = 0, + .cb = filesystem_cmd_df, + }, + { + .name = "rm", + .help = "usage: rm [PATH]", + .min_args = 1, + .max_args = 1, + .cb = filesystem_cmd_rm, + }, + { + .name = "echo", + .help = "usage: echo TEXT {>,>>} FILE", + .min_args = 3, + .max_args = 3, + .cb = filesystem_cmd_echo, + }, + { + .name = "stress", + .help = "test CDC write; usage: stress [LEN] [DELAY_MS]", + .min_args = 0, + .max_args = 2, + .cb = stress_cmd, + }, +}; + +const size_t g_num_shell_commands = sizeof(g_shell_commands) / sizeof(shell_command_t); + +static int help_cmd(int argc, char *argv[]) { + (void) argc; + (void) argv; + + printf("Command List:\r\n"); + for (size_t i = 0; i < g_num_shell_commands; i++) { + printf(" %s\t%s\r\n", + g_shell_commands[i].name, + (g_shell_commands[i].help) ? g_shell_commands[i].help : "" + ); + } + + return 0; +} + +static int flash_cmd(int argc, char *argv[]) { + (void) argc; + (void) argv; + + watch_reset_to_bootloader(); + return 0; +} + +#define STRESS_CMD_MAX_LEN (512) +static int stress_cmd(int argc, char *argv[]) { + char test_str[STRESS_CMD_MAX_LEN+1] = {0}; + + int max_len = 512; + int delay = 0; + + if (argc >= 2) { + if ((max_len = atoi(argv[1])) == 0) { + return -1; + } + if (max_len > 512) { + return -1; + } + } + + if (argc >= 3) { + delay = atoi(argv[2]); + } + + for (int i = 0; i < max_len; i++) { + snprintf(&test_str[i], 2, "%u", (i+1)%10); + printf("%u:\t%s\r\n", (i+1), test_str); + if (delay > 0) { + delay_ms(delay); + } + } + + return 0; +} + diff --git a/movement/shell_cmd_list.h b/movement/shell_cmd_list.h new file mode 100644 index 0000000..89031a5 --- /dev/null +++ b/movement/shell_cmd_list.h @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2023 Edward Shin + * + * 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 SHELL_CMD_LIST_H_ +#define SHELL_CMD_LIST_H_ + +#include + +typedef struct { + const char *name; // Name used to invoke the command + const char *help; // Help string + int8_t min_args; // Minimum number of arguments (_excluding_ the command name) + int8_t max_args; // Maximum number of arguments (_excluding_ the command name) + int (*cb)(int argc, char *argv[]); // Callback for the command +} shell_command_t; + +#endif diff --git a/movement/template/template.c b/movement/template/template.c index e03db56..fe2723b 100644 --- a/movement/template/template.c +++ b/movement/template/template.c @@ -28,6 +28,7 @@ void <#watch_face_name#>_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(<#watch_face_name#>_state_t)); memset(*context_ptr, 0, sizeof(<#watch_face_name#>_state_t)); diff --git a/movement/watch_faces/clock/beats_face.c b/movement/watch_faces/clock/beats_face.c index 50c3284..85bcbe0 100644 --- a/movement/watch_faces/clock/beats_face.c +++ b/movement/watch_faces/clock/beats_face.c @@ -1,3 +1,27 @@ +/* + * MIT License + * + * Copyright (c) 2023 Wesley Ellis + * + * 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 "beats_face.h" diff --git a/movement/watch_faces/clock/beats_face.h b/movement/watch_faces/clock/beats_face.h index 2bbbc26..4a06624 100644 --- a/movement/watch_faces/clock/beats_face.h +++ b/movement/watch_faces/clock/beats_face.h @@ -1,6 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2023 Wesley Ellis + * + * 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 BEATS_FACE_H_ #define BEATS_FACE_H_ +/* + * BEATS TIME face + * + * The Beat Time face displays the current Swatch Internet Time, or .beat time. + * This is a decimal time system that divides the day into 1000 beats. + * + * The three large digits in the bottom row indicate the current beat, and the + * two smaller digits (normally the seconds in Simple Clock) indicate the + * fractional beat; so for example you can read “67214” as “beat 672.14”. + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/clock/clock_face.c b/movement/watch_faces/clock/clock_face.c new file mode 100644 index 0000000..eab5cd8 --- /dev/null +++ b/movement/watch_faces/clock/clock_face.c @@ -0,0 +1,291 @@ +/* SPDX-License-Identifier: MIT */ + +/* + * MIT License + * + * Copyright © 2021-2023 Joey Castillo + * Copyright © 2022 David Keck + * Copyright © 2022 TheOnePerson + * Copyright © 2023 Jeremy O'Brien + * Copyright © 2023 Mikhail Svarichevsky <3@14.by> + * Copyright © 2023 Wesley Aptekar-Cassels + * Copyright © 2024 Matheus Afonso Martins Moreira + * + * 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 "clock_face.h" +#include "watch.h" +#include "watch_utility.h" +#include "watch_private_display.h" + +// 2.2 volts will happen when the battery has maybe 5-10% remaining? +// we can refine this later. +#ifndef CLOCK_FACE_LOW_BATTERY_VOLTAGE_THRESHOLD +#define CLOCK_FACE_LOW_BATTERY_VOLTAGE_THRESHOLD 2200 +#endif + +#ifndef CLOCK_FACE_24H_ONLY +#define CLOCK_FACE_24H_ONLY 0 +#endif + +typedef struct { + struct { + watch_date_time previous; + } date_time; + uint8_t last_battery_check; + uint8_t watch_face_index; + bool time_signal_enabled; + bool battery_low; +} clock_state_t; + +static bool clock_is_in_24h_mode(movement_settings_t *settings) { + if (CLOCK_FACE_24H_ONLY) { return true; } + return settings->bit.clock_mode_24h; +} + +static void clock_indicate(WatchIndicatorSegment indicator, bool on) { + if (on) { + watch_set_indicator(indicator); + } else { + watch_clear_indicator(indicator); + } +} + +static void clock_indicate_alarm(movement_settings_t *settings) { + clock_indicate(WATCH_INDICATOR_BELL, settings->bit.alarm_enabled); +} + +static void clock_indicate_time_signal(clock_state_t *clock) { + clock_indicate(WATCH_INDICATOR_SIGNAL, clock->time_signal_enabled); +} + +static void clock_indicate_24h(movement_settings_t *settings) { + clock_indicate(WATCH_INDICATOR_24H, clock_is_in_24h_mode(settings)); +} + +static bool clock_is_pm(watch_date_time date_time) { + return date_time.unit.hour >= 12; +} + +static void clock_indicate_pm(movement_settings_t *settings, watch_date_time date_time) { + if (settings->bit.clock_mode_24h) { return; } + clock_indicate(WATCH_INDICATOR_PM, clock_is_pm(date_time)); +} + +static void clock_indicate_low_available_power(clock_state_t *clock) { + // Set the LAP indicator if battery power is low + clock_indicate(WATCH_INDICATOR_LAP, clock->battery_low); +} + +static watch_date_time clock_24h_to_12h(watch_date_time date_time) { + date_time.unit.hour %= 12; + + if (date_time.unit.hour == 0) { + date_time.unit.hour = 12; + } + + return date_time; +} + +static void clock_check_battery_periodically(clock_state_t *clock, watch_date_time date_time) { + // check the battery voltage once a day + if (date_time.unit.day == clock->last_battery_check) { return; } + + clock->last_battery_check = date_time.unit.day; + + watch_enable_adc(); + uint16_t voltage = watch_get_vcc_voltage(); + watch_disable_adc(); + + clock->battery_low = voltage < CLOCK_FACE_LOW_BATTERY_VOLTAGE_THRESHOLD; + + clock_indicate_low_available_power(clock); +} + +static void clock_toggle_time_signal(clock_state_t *clock) { + clock->time_signal_enabled = !clock->time_signal_enabled; + clock_indicate_time_signal(clock); +} + +static void clock_display_all(watch_date_time date_time) { + char buf[10 + 1]; + + snprintf( + buf, + sizeof(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, 0); +} + +static bool clock_display_some(watch_date_time current, watch_date_time previous) { + if ((current.reg >> 6) == (previous.reg >> 6)) { + // everything before seconds is the same, don't waste cycles setting those segments. + + watch_display_character_lp_seconds('0' + current.unit.second / 10, 8); + watch_display_character_lp_seconds('0' + current.unit.second % 10, 9); + + return true; + + } else if ((current.reg >> 12) == (previous.reg >> 12)) { + // everything before minutes is the same. + + char buf[4 + 1]; + + snprintf( + buf, + sizeof(buf), + "%02d%02d", + current.unit.minute, + current.unit.second + ); + + watch_display_string(buf, 6); + + return true; + + } else { + // other stuff changed; let's do it all. + return false; + } +} + +static void clock_display_clock(movement_settings_t *settings, clock_state_t *clock, watch_date_time current) { + if (!clock_display_some(current, clock->date_time.previous)) { + if (!clock_is_in_24h_mode(settings)) { + // if we are in 12 hour mode, do some cleanup. + clock_indicate_pm(settings, current); + current = clock_24h_to_12h(current); + } + clock_display_all(current); + } +} + +static void clock_display_low_energy(watch_date_time date_time) { + char buf[10 + 1]; + + snprintf( + buf, + sizeof(buf), + "%s%2d%2d%02d ", + watch_utility_get_weekday(date_time), + date_time.unit.day, + date_time.unit.hour, + date_time.unit.minute + ); + + watch_display_string(buf, 0); +} + +static void clock_start_tick_tock_animation(void) { + if (!watch_tick_animation_is_running()) { + watch_start_tick_animation(500); + } +} + +static void clock_stop_tick_tock_animation(void) { + if (watch_tick_animation_is_running()) { + watch_stop_tick_animation(); + } +} + +void clock_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(clock_state_t)); + clock_state_t *state = (clock_state_t *) *context_ptr; + state->time_signal_enabled = false; + state->watch_face_index = watch_face_index; + } +} + +void clock_face_activate(movement_settings_t *settings, void *context) { + clock_state_t *clock = (clock_state_t *) context; + + clock_stop_tick_tock_animation(); + + clock_indicate_time_signal(clock); + clock_indicate_alarm(settings); + clock_indicate_24h(settings); + + watch_set_colon(); + + // this ensures that none of the timestamp fields will match, so we can re-render them all. + clock->date_time.previous.reg = 0xFFFFFFFF; +} + +bool clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + clock_state_t *state = (clock_state_t *) context; + watch_date_time current; + + switch (event.event_type) { + case EVENT_LOW_ENERGY_UPDATE: + clock_start_tick_tock_animation(); + clock_display_low_energy(watch_rtc_get_date_time()); + break; + case EVENT_TICK: + case EVENT_ACTIVATE: + current = watch_rtc_get_date_time(); + + clock_display_clock(settings, state, current); + + clock_check_battery_periodically(state, current); + + state->date_time.previous = current; + + break; + case EVENT_ALARM_LONG_PRESS: + clock_toggle_time_signal(state); + break; + case EVENT_BACKGROUND_TASK: + // uncomment this line to snap back to the clock face when the hour signal sounds: + // movement_move_to_face(state->watch_face_index); + movement_play_signal(); + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void clock_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + +bool clock_face_wants_background_task(movement_settings_t *settings, void *context) { + (void) settings; + clock_state_t *state = (clock_state_t *) context; + if (!state->time_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/clock_face.h b/movement/watch_faces/clock/clock_face.h new file mode 100644 index 0000000..c4209e3 --- /dev/null +++ b/movement/watch_faces/clock/clock_face.h @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: MIT */ + +/* + * MIT License + * + * Copyright © 2021-2022 Joey Castillo + * Copyright © 2022 Alexsander Akers + * Copyright © 2022 TheOnePerson + * Copyright © 2023 Alex Utter + * Copyright © 2024 Matheus Afonso Martins Moreira + * + * 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 CLOCK_FACE_H_ +#define CLOCK_FACE_H_ + +/* + * CLOCK FACE + * + * Displays the current local time, just like the original watch. + * This is the default display mode in most watch configurations. + * + * Long-press ALARM to toggle the hourly chime. + * + */ + +#include "movement.h" + +void clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void clock_face_activate(movement_settings_t *settings, void *context); +bool clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void clock_face_resign(movement_settings_t *settings, void *context); +bool clock_face_wants_background_task(movement_settings_t *settings, void *context); + +#define clock_face ((const watch_face_t) { \ + clock_face_setup, \ + clock_face_activate, \ + clock_face_loop, \ + clock_face_resign, \ + clock_face_wants_background_task, \ +}) + +#endif // CLOCK_FACE_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 0000000..86f07f9 --- /dev/null +++ b/movement/watch_faces/clock/day_night_percentage_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. + */ + +#include +#include +#include +#include "day_night_percentage_face.h" +#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; + + 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) { + (void) settings; + (void) context; +} + +bool day_night_percentage_face_loop(movement_event_t event, movement_settings_t *settings, 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: + if ((utc_now.unit.hour == 0 && utc_now.unit.minute == 0 && utc_now.unit.second == 0) || state->result == -2) { + recalculate(utc_now, state); + } + + if (state->result == -2) { + watch_display_string(" no Loc", 0); + break; + } + + const char* weekday = watch_utility_get_weekday(date_time); + if (state->result != 0) { + 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; + + 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; + if (day_percentage > 0.0 && day_percentage < 1.0) { + percentage = day_percentage * 10000; + watch_clear_indicator(WATCH_INDICATOR_PM); + } else { + percentage = night_percentage * 10000; + 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 ", weekday, date_time.unit.day, percentage / 100); + } else { + sprintf(buf, "%s%2d %04d", weekday, 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 0000000..d172c74 --- /dev/null +++ b/movement/watch_faces/clock/day_night_percentage_face.h @@ -0,0 +1,66 @@ +/* + * 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 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. + */ + +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); +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_ + diff --git a/movement/watch_faces/clock/decimal_time_face.h b/movement/watch_faces/clock/decimal_time_face.h index b9f14f3..3d3a913 100644 --- a/movement/watch_faces/clock/decimal_time_face.h +++ b/movement/watch_faces/clock/decimal_time_face.h @@ -25,10 +25,8 @@ #ifndef DECIMAL_TIME_FACE_H_ #define DECIMAL_TIME_FACE_H_ -#include "movement.h" - /* - * DECIMAL TIME FACE + * DECIMAL TIME face * * This face presents the current time as hours and hundredths of an hour. Every hundreth of an hour, or "centihour", * occurs every 36 seconds. Because they range from 0 to 99, centihours, in the seventies range, will be displayed with a lowercase 7. @@ -46,9 +44,10 @@ * https://hr.colostate.edu/minute-to-decimal-conversion-chart/ * * Many thanks go to Joey Castillo for making this project happen. - * */ +#include "movement.h" + typedef struct { bool chime_enabled; // did the user enable hourly chime for this face? uint8_t features_to_show : 2 ; // what features are to be displayed? diff --git a/movement/watch_faces/clock/mars_time_face.h b/movement/watch_faces/clock/mars_time_face.h index d34792e..0491768 100644 --- a/movement/watch_faces/clock/mars_time_face.h +++ b/movement/watch_faces/clock/mars_time_face.h @@ -25,6 +25,32 @@ #ifndef MARS_TIME_FACE_H_ #define MARS_TIME_FACE_H_ +/* + * MARS TIME face + * + * This watch face is dedicated to Martian timekeeping. + * It has several modes, and can display either a time or a date. + * + * Pressing the ALARM button cycles through different time zones on Mars: + * MC - Mars Coordinated Time, the time at Airy-0 Crater on the Martian prime meridian + * ZH - Local mean solar time for the Zhurong rover + * PE - LMST for the Perseverance rover + * IN - LMST for the Insight lander + * CU - LMST for the Curiosity rover + * + * Press the LIGHT button to toggle between displaying time and date: + * MC S - the Mars Sol Date, Martian days since December 29, 1873 + * ZH Sol - Mission sol for the Zhurong rover + * PE Sol - Mission sol for the Perseverance rover + * IN S - Mission sol for the InSight lander + * CU S - Mission sol for the Curiosity rover + * + * Note that where the mission sol is below 1000, this watch face displays + * the word “Sol” on the bottom line. When the mission sol is over 1000, the + * word “Sol” will not fit and so it displays a stylized letter S at the top + * right. + */ + #include "movement.h" typedef enum { 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 0000000..2cedc30 --- /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 0000000..4bc9a8b --- /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_ diff --git a/movement/watch_faces/clock/repetition_minute_face.c b/movement/watch_faces/clock/repetition_minute_face.c index a0fbe07..e9e5e31 100644 --- a/movement/watch_faces/clock/repetition_minute_face.c +++ b/movement/watch_faces/clock/repetition_minute_face.c @@ -153,17 +153,7 @@ bool repetition_minute_face_loop(movement_event_t event, movement_settings_t *se case EVENT_BACKGROUND_TASK: // uncomment this line to snap back to the clock face when the hour signal sounds: // movement_move_to_face(state->watch_face_index); - if (watch_is_buzzer_or_led_enabled()) { - // if we are in the foreground, we can just beep. - movement_play_signal(); - } else { - // if we were in the background, we need to enable the buzzer peripheral first, - watch_enable_buzzer(); - // beep quickly (this call blocks for 275 ms), - movement_play_signal(); - // and then turn the buzzer peripheral off again. - watch_disable_buzzer(); - } + movement_play_signal(); break; case EVENT_LIGHT_LONG_UP: /* diff --git a/movement/watch_faces/clock/repetition_minute_face.h b/movement/watch_faces/clock/repetition_minute_face.h index 5a897bc..8c3100d 100644 --- a/movement/watch_faces/clock/repetition_minute_face.h +++ b/movement/watch_faces/clock/repetition_minute_face.h @@ -25,9 +25,9 @@ #ifndef REPETITION_MINUTE_FACE_H_ #define REPETITION_MINUTE_FACE_H_ -#include "movement.h" - /* + * REPETITION MINUTE face + * * A hopefully useful complication for friendly neighbors in the dark * * Originating from 1676 from reverend and mechanician Edward Barlow, and @@ -40,7 +40,6 @@ * 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 : @@ -51,9 +50,10 @@ * Prerequisite : a watch with a working buzzer * * ~ Only in the darkness can you see the stars. - Martin Luther King ~ - * */ +#include "movement.h" + typedef struct { uint32_t previous_date_time; uint8_t last_battery_check; diff --git a/movement/watch_faces/clock/simple_clock_bin_led_face.c b/movement/watch_faces/clock/simple_clock_bin_led_face.c index 640f0d7..cf39c18 100644 --- a/movement/watch_faces/clock/simple_clock_bin_led_face.c +++ b/movement/watch_faces/clock/simple_clock_bin_led_face.c @@ -180,17 +180,7 @@ bool simple_clock_bin_led_face_loop(movement_event_t event, movement_settings_t case EVENT_BACKGROUND_TASK: // uncomment this line to snap back to the clock face when the hour signal sounds: // movement_move_to_face(state->watch_face_index); - if (watch_is_buzzer_or_led_enabled()) { - // if we are in the foreground, we can just beep. - movement_play_signal(); - } else { - // if we were in the background, we need to enable the buzzer peripheral first, - watch_enable_buzzer(); - // beep quickly (this call blocks for 275 ms), - movement_play_signal(); - // and then turn the buzzer peripheral off again. - watch_disable_buzzer(); - } + movement_play_signal(); break; case EVENT_LIGHT_LONG_PRESS: if (state->flashing_state == 0) { diff --git a/movement/watch_faces/clock/simple_clock_bin_led_face.h b/movement/watch_faces/clock/simple_clock_bin_led_face.h index 918b82a..79c7c14 100644 --- a/movement/watch_faces/clock/simple_clock_bin_led_face.h +++ b/movement/watch_faces/clock/simple_clock_bin_led_face.h @@ -25,9 +25,9 @@ #ifndef SIIMPLE_CLOCK_BIN_LED_FACE_H_ #define SIIMPLE_CLOCK_BIN_LED_FACE_H_ -#include "movement.h" - /* + * BINARY LED CLOCK FACE + * * A "fork" of the simple clock face, which provides the functionality of showing * the current time by flashing the LED using binary representation. * @@ -49,6 +49,8 @@ * represents 1. */ +#include "movement.h" + typedef struct { uint32_t previous_date_time; uint8_t last_battery_check; diff --git a/movement/watch_faces/clock/simple_clock_face.c b/movement/watch_faces/clock/simple_clock_face.c index 91400b6..fbc2c4b 100644 --- a/movement/watch_faces/clock/simple_clock_face.c +++ b/movement/watch_faces/clock/simple_clock_face.c @@ -136,17 +136,7 @@ bool simple_clock_face_loop(movement_event_t event, movement_settings_t *setting case EVENT_BACKGROUND_TASK: // uncomment this line to snap back to the clock face when the hour signal sounds: // movement_move_to_face(state->watch_face_index); - if (watch_is_buzzer_or_led_enabled()) { - // if we are in the foreground, we can just beep. - movement_play_signal(); - } else { - // if we were in the background, we need to enable the buzzer peripheral first, - watch_enable_buzzer(); - // beep quickly (this call blocks for 275 ms), - movement_play_signal(); - // and then turn the buzzer peripheral off again. - watch_disable_buzzer(); - } + movement_play_signal(); break; default: return movement_default_loop_handler(event, settings); diff --git a/movement/watch_faces/clock/simple_clock_face.h b/movement/watch_faces/clock/simple_clock_face.h index 1e9baba..e74a6e8 100644 --- a/movement/watch_faces/clock/simple_clock_face.h +++ b/movement/watch_faces/clock/simple_clock_face.h @@ -25,6 +25,15 @@ #ifndef SIMPLE_CLOCK_FACE_H_ #define SIMPLE_CLOCK_FACE_H_ +/* + * SIMPLE CLOCK FACE + * + * Displays the current time, matching the original operation of the watch. + * This is the default display mode in most watch configurations. + * + * Long-press ALARM to toggle the hourly chime. + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/clock/weeknumber_clock_face.c b/movement/watch_faces/clock/weeknumber_clock_face.c index 4e40ebd..81df584 100644 --- a/movement/watch_faces/clock/weeknumber_clock_face.c +++ b/movement/watch_faces/clock/weeknumber_clock_face.c @@ -130,17 +130,7 @@ bool weeknumber_clock_face_loop(movement_event_t event, movement_settings_t *set case EVENT_BACKGROUND_TASK: // uncomment this line to snap back to the clock face when the hour signal sounds: // movement_move_to_face(state->watch_face_index); - if (watch_is_buzzer_or_led_enabled()) { - // if we are in the foreground, we can just beep. - movement_play_signal(); - } else { - // if we were in the background, we need to enable the buzzer peripheral first, - watch_enable_buzzer(); - // beep quickly (this call blocks for 275 ms), - movement_play_signal(); - // and then turn the buzzer peripheral off again. - watch_disable_buzzer(); - } + movement_play_signal(); break; default: movement_default_loop_handler(event, settings); diff --git a/movement/watch_faces/clock/weeknumber_clock_face.h b/movement/watch_faces/clock/weeknumber_clock_face.h index f0298ea..ec61942 100644 --- a/movement/watch_faces/clock/weeknumber_clock_face.h +++ b/movement/watch_faces/clock/weeknumber_clock_face.h @@ -25,6 +25,14 @@ #ifndef WEEKNUMBER_CLOCK_FACE_H_ #define WEEKNUMBER_CLOCK_FACE_H_ +/* + * WEEK-NUMBER WATCH FACE + * + * Same as simple clock, but has iso 8601 week number instead of seconds counter. + * + * Long-press ALARM to toggle the hourly chime. + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/clock/world_clock2_face.c b/movement/watch_faces/clock/world_clock2_face.c index 0077f63..2e1e969 100644 --- a/movement/watch_faces/clock/world_clock2_face.c +++ b/movement/watch_faces/clock/world_clock2_face.c @@ -23,79 +23,6 @@ * SOFTWARE. */ -/* - * World Clock 2 - * ============= - * - * This is an alternative world clock face that allows the user to cycle - * through a list of selected time zones. It extends the original - * implementation by Joey Castillo. The face has two modes *display mode* - * and *settings mode*. - * - * ### Settings mode - * - * When the clock face is activated for the first time, it enters - * *settings mode*. Here, the user can select the time zones they want to - * display. The face shows a summary of the current time zone: - * - * - The top of the face displays the first two letters of the time zone - * abbreviation, such as "PS" for Pacific Standard Time or CE for - * "Central European Time". - * - * - The upper-right corner shows the index number of the time zone. This - * helps avoid confusion when multiple time zones have the same - * two-letter abbreviation. - * - * - The main display shows the offset from UTC, with a "+" indicating a - * positive offset and a "-" indicating a negative offset. For example, - * the offset for Japanese Standard Time is displayed as "+9:00". - * - * The user can navigate through the time zones and select them using the - * following buttons: - * - * - The *alarm button* moves forward to the next time zone, while the - * *light button* moves backward to the previous zone. This way, the - * user can cycle through all 41 supported time zones. - * - * - A *long press* on the *light button* selects the current time zone, - * and the signal indicator appears at the top left. Another *long - * press* of the *light button* deselects the time zone. - * - * - A *long press* on the *alarm button* exits settings mode and returns - * to display mode. - * - * ### Display mode - * - * In the display mode, the face shows the time of the currently selected - * time zone. The face includes the following components: - * - * - The top of the face displays the first two letters of the time zone - * abbreviation, such as "PS" for Pacific Standard Time or "CE" for - * Central European Time. - * - * - The upper-right corner shows the current day of the month, which - * helps indicate time zones that cross the international date line - * with respect to the local time. - * - * - The main display shows the time in the selected time zone in either - * 12-hour or 24-hour form. There is no timeout, allowing users to keep - * the chosen time zone displayed for as long as they wish. - * - * The user can navigate through the selected time zones using the - * following buttons: - * - * - The *alarm button* moves to the next selected time zone, while the - * light button moves to the *previous zone*. If no time zone is - * selected, the face simply shows UTC. - * - * - A *long press* on the *alarm button* enters settings mode and - * enables the user to re-configure the selected time zones. - * - * - A *long press* on the *light button* activates the LED illumination - * of the watch. - * - */ - #include #include #include "world_clock2_face.h" diff --git a/movement/watch_faces/clock/world_clock2_face.h b/movement/watch_faces/clock/world_clock2_face.h index f70dca1..0baac21 100644 --- a/movement/watch_faces/clock/world_clock2_face.h +++ b/movement/watch_faces/clock/world_clock2_face.h @@ -26,6 +26,65 @@ #ifndef WORLD_CLOCK2_FACE_H_ #define WORLD_CLOCK2_FACE_H_ +/* + * WORLD CLOCK 2 + * + * This is an alternative world clock face that allows the user to cycle + * through a list of selected time zones. It extends the original + * implementation by Joey Castillo. The face has two modes: display mode + * and settings mode. + * + * Settings mode + * + * When the clock face is activated for the first time, it enters settings + * mode. Here, the user can select the time zones they want to display. The + * face shows a summary of the current time zone: + * * The top of the face displays the first two letters of the time zone + * abbreviation, such as "PS" for Pacific Standard Time or CE for + * "Central European Time". + * * The upper-right corner shows the index number of the time zone. This + * helps avoid confusion when multiple time zones have the same two-letter + * abbreviation. + * * The main display shows the offset from UTC, with a "+" indicating a + * positive offset and a "-" indicating a negative offset. For example, + * the offset for Japanese Standard Time is displayed as "+9:00". + * + * The user can navigate through the time zones and select them using the + * following buttons: + * * The ALARM button moves forward to the next time zone, while the LIGHT + * button moves backward to the previous zone. This way, the user can + * cycle through all 41 supported time zones. + * * A long press on the LIGHT button selects the current time zone, and + * the signal indicator appears at the top left. Another long press of + * the LIGHT button deselects the time zone. + * * A long press on the ALARM button exits settings mode and returns to + * display mode. + * + * Display mode + * + * In the display mode, the face shows the time of the currently selected + * time zone. The face includes the following components: + * * The top of the face displays the first two letters of the time zone + * abbreviation, such as "PS" for Pacific Standard Time or "CE" for + * Central European Time. + * * The upper-right corner shows the current day of the month, which helps + * indicate time zones that cross the international date line with respect + * to the local time. + * * The main display shows the time in the selected time zone in either + * 12-hour or 24-hour form. There is no timeout, allowing users to keep + * the chosen time zone displayed for as long as they wish. + * + * The user can navigate through the selected time zones using the following + * buttons: + * * The ALARM button moves to the next selected time zone, while the LIGHT + * button moves to the previous zone. If no time zone is selected, the + * face simply shows UTC. + * * A long press on the ALARM button enters settings mode and enables the + * user to re-configure the selected time zones. + * * A long press on the LIGHT button activates the LED illumination of the + * watch. + */ + /* Number of zones. See movement_timezone_offsets. */ #define NUM_TIME_ZONES 41 diff --git a/movement/watch_faces/clock/world_clock_face.h b/movement/watch_faces/clock/world_clock_face.h index 669dcaa..92e91a6 100644 --- a/movement/watch_faces/clock/world_clock_face.h +++ b/movement/watch_faces/clock/world_clock_face.h @@ -25,7 +25,29 @@ #ifndef WORLD_CLOCK_FACE_H_ #define WORLD_CLOCK_FACE_H_ +/* + * WORLD CLOCK FACE + * + * The World Clock watch face looks similar to the Simple Clock watch face, + * but you’ll notice that at first launch the day of week indicators are blank. + * That’s because this watch face does not display the day of the week. + * Instead, you may customize these letters to display the name of a time zone + * of your choosing. + * + * To customize this watch face, press and hold the ALARM button. The first + * letter in the top row will begin flashing. Press the ALARM button repeatedly + * to advance through the available letters in the first slot, then press the + * LIGHT button to move to the second letter. Finally, press LIGHT again to move + * to the time zone setting, and press ALARM to cycle through the available time + * zones. Press LIGHT one last time to return to the world clock display. + * + * Note that the second slot cannot display all letters or numbers. Also note + * that at this time, time zones do not automatically update for daylight saving + * time; you will need to manually adjust this field each spring and fall. + */ + #include "movement.h" + typedef union { struct { uint8_t char_0; diff --git a/movement/watch_faces/clock/wyoscan_face.c b/movement/watch_faces/clock/wyoscan_face.c index fd87b91..66a5d46 100644 --- a/movement/watch_faces/clock/wyoscan_face.c +++ b/movement/watch_faces/clock/wyoscan_face.c @@ -60,7 +60,7 @@ a line you've already drawn. It is vaguely top to bottom and counter, clockwise when possible. */ static char *segment_map[] = { - "AXFEDCBX", // 0 + "AXFBDEXC", // 0 "BXXXCXXX", // 1 "ABGEXXXD", // 2 "ABGXXXCD", // 3 diff --git a/movement/watch_faces/clock/wyoscan_face.h b/movement/watch_faces/clock/wyoscan_face.h index 68db9ee..d20e763 100644 --- a/movement/watch_faces/clock/wyoscan_face.h +++ b/movement/watch_faces/clock/wyoscan_face.h @@ -25,15 +25,32 @@ #ifndef WYOSCAN_FACE_H_ #define WYOSCAN_FACE_H_ -#include "movement.h" - /* - * A DESCRIPTION OF YOUR WATCH FACE + * WYOSCAN .5 hz watchface * - * and a description of how use it + * This is a recreation of the Wyoscan watch, which was a $175 watch in 2014. + * It was an f-91w pcb replacement. + * + * Video: https://user-images.githubusercontent.com/1795778/252550124-e07f0ed1-e328-4337-a654-fa1ee65d883f.mp4 + * Background information: https://artmetropole.com/shop/11460 + * Demo of what it looks like: https://www.o-r-g.com/apps/wyoscan + * + * 8 frames per number * 6 numbers + the trailing 16 frames = 64 frames + * at 32 frames per second, this is a 2-second cycle time or 0.5 Hz. + * + * It is giving me a stack overflow after about 2.5 cycles of the time display + * in the emulator, but it works fine on the watch. + * + * I'd like to make something for the low energy mode, but I haven't thought + * about how that might work, right now it just freezes in low energy mode + * until you press the 12-24HR button. + * + * There are no controls; it simply animates as long as the page is active. * */ +#include "movement.h" + #define MAX_ILLUMINATED_SEGMENTS 16 typedef struct { diff --git a/movement/watch_faces/complication/activity_face.h b/movement/watch_faces/complication/activity_face.h index c72f709..552ef4d 100644 --- a/movement/watch_faces/complication/activity_face.h +++ b/movement/watch_faces/complication/activity_face.h @@ -25,10 +25,8 @@ #ifndef ACTIVITY_FACE_H_ #define ACTIVITY_FACE_H_ -#include "movement.h" - /* - * ACTIVITY WATCH FACE + * ACTIVITY watch face * * The Activity face lets you record activities like you would do with a fitness watch. * It supports different activities like running, biking, rowing etc., and for each recorded activity @@ -69,9 +67,10 @@ * * See the top of activity_face.c for some customization options. What you most likely want to do * is reduce the list of activities shown on the first screen to the ones you are regularly doing. - * */ +#include "movement.h" + void activity_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); void activity_face_activate(movement_settings_t *settings, void *context); bool activity_face_loop(movement_event_t event, movement_settings_t *settings, void *context); diff --git a/movement/watch_faces/complication/alarm_face.c b/movement/watch_faces/complication/alarm_face.c index 3b7d1e3..3cacc98 100644 --- a/movement/watch_faces/complication/alarm_face.c +++ b/movement/watch_faces/complication/alarm_face.c @@ -22,8 +22,6 @@ * SOFTWARE. */ -//----------------------------------------------------------------------------- - #include #include @@ -32,31 +30,6 @@ #include "watch_utility.h" #include "watch_private_display.h" -/* - Implements 16 alarm slots on the sensor watch - - Usage: - - In normal mode, the alarm button cycles through all 16 alarms. - - Pressing the alarm button long in normal mode toggles the corresponding alarm on or off. - (Whereas pressing the alarm button extra long brings you back to alarm no. 1.) - - Pressing the light button enters setting mode and cycles through the settings of each alarm. - (Long pressing the light button enters setting mode without illuminating the led.) - - In setting mode an alarm slot is selected by pressing the alarm button when the slot number - in the upper right corner is blinking. - - For each alarm slot, you can select the day. These are the day modes: - - ED = the alarm rings every day - - 1t = the alarm fires only one time and is erased afterwards - - MF = the alarm fires Mondays to Fridays - - WN = the alarm fires on weekends (Sa/Su) - - MO to SU = the alarm fires only on the given day of week - - You can fast cycle through hour or minute setting via long press of the alarm button. - - You can select the tone in which the alarm is played. (Three pitch levels available.) - - You can select how many "beep rounds" are played for each alarm. 1 to 9 rounds, plus extra - long ('L') and extra short ('o') alarms. - - The simple watch face indicates if any alarm is set within the next 24h by showing the signal - indicator. -*/ - typedef enum { alarm_setting_idx_alarm, alarm_setting_idx_day, diff --git a/movement/watch_faces/complication/alarm_face.h b/movement/watch_faces/complication/alarm_face.h index dafbee5..1c22948 100644 --- a/movement/watch_faces/complication/alarm_face.h +++ b/movement/watch_faces/complication/alarm_face.h @@ -27,11 +27,34 @@ #ifndef ALARM_FACE_H_ #define ALARM_FACE_H_ -#include "movement.h" - /* -A face for setting various alarms -*/ + * ALARM face + * + * Implements up to 16 alarm slots on the sensor watch + * + * Usage: + * - In normal mode, the alarm button cycles through all 16 alarms. + * - Pressing the alarm button long in normal mode toggles the corresponding alarm on or off. + * (Whereas pressing the alarm button extra long brings you back to alarm no. 1.) + * - Pressing the light button enters setting mode and cycles through the settings of each alarm. + * (Long pressing the light button enters setting mode without illuminating the led.) + * - In setting mode an alarm slot is selected by pressing the alarm button when the slot number + * in the upper right corner is blinking. + * - For each alarm slot, you can select the day. These are the day modes: + * - ED = the alarm rings every day + * - 1t = the alarm fires only one time and is erased afterwards + * - MF = the alarm fires Mondays to Fridays + * - WN = the alarm fires on weekends (Sa/Su) + * - MO to SU = the alarm fires only on the given day of week + * - You can fast cycle through hour or minute setting via long press of the alarm button. + * - You can select the tone in which the alarm is played. (Three pitch levels available.) + * - You can select how many "beep rounds" are played for each alarm. 1 to 9 rounds, plus extra + * long ('L') and extra short ('o') alarms. + * - The simple watch face indicates if any alarm is set within the next 24h by showing the signal + * indicator. + */ + +#include "movement.h" #define ALARM_ALARMS 16 // no of available alarm slots (be aware: only 4 bits reserved for this value in struct below) #define ALARM_DAY_STATES 11 // no of different day settings diff --git a/movement/watch_faces/complication/astronomy_face.h b/movement/watch_faces/complication/astronomy_face.h index f956955..6ab2211 100644 --- a/movement/watch_faces/complication/astronomy_face.h +++ b/movement/watch_faces/complication/astronomy_face.h @@ -25,6 +25,47 @@ #ifndef ASTRONOMY_FACE_H_ #define ASTRONOMY_FACE_H_ +/* + * ASTRONOMY face + * + * The Astronomy watch face is among the most complex watch faces in the + * Movement collection. It allows you to calculate the locations of celestial + * bodies in the sky, as well as distance in astronomical units (or, in the + * case of the Moon, distance in kilometers). + * + * When you arrive at the Astronomy watch face, you’ll see its name (“Astro”) + * and an animation of two objects orbiting each other. You will also see “SO” + * (for Sol) flashing in the top left. The flashing letters indicate the + * currently selected celestial body. Short press Alarm to advance through + * the available celestial bodies: + * + * SO - Sol, the sun + * ME - Mercury + * VE - Venus + * LU - Luna, the Earth’s moon + * MA - Mars + * JU - Jupiter + * SA - Saturn + * UR - Uranus + * NE - Neptune + * + * Once you’ve selected the celestial body whose parameters you wish to + * calculate, long press the Alarm button and release it. The letter “C” will + * flash while the calculation is performed. + * + * When the calculation is complete, the screen will display the altitude + * (“aL”) of the celestial body. You can cycle through the available parameters + * with repeated short presses on the Alarm button: + * + * aL - Altitude (in degrees), the elevation over the horizon. If negative, it is below the horizon. + * aZ - Azimuth (in degrees), the cardinal direction relative to true north. + * rA - Right Ascension (in hours/minutes/seconds) + * dE - Declination (in degrees/minutes/seconds) + * di - Distance (the digits in the top right will display either aU for astronomical units, or K for kilometers) + * + * Long press on the Alarm button to select another celestial body. + */ + #include "movement.h" #include "astrolib.h" diff --git a/movement/watch_faces/complication/blinky_face.h b/movement/watch_faces/complication/blinky_face.h index e966ab1..f453de6 100644 --- a/movement/watch_faces/complication/blinky_face.h +++ b/movement/watch_faces/complication/blinky_face.h @@ -25,6 +25,32 @@ #ifndef BLINKY_FACE_H_ #define BLINKY_FACE_H_ +/* + * BLINKY LIGHT face + * + * The blinky light watch face was designed as a tutorial for making a watch + * face in Movement, but it actually might be useful to have a blinking light + * in a pinch. + * + * The screen displays the name of the watch face (”BL”), as well as an S at + * the top right for slow blink or an F for fast blink. The bottom line selects + * the color: green, red or yellow. You can change the speed of the blinking + * light by pressing the Alarm button, and change the color with the Light + * button. A long press on the Alarm button starts the blinking light, and + * another long press stops it. + * + * Note that this will chew through your battery! The green LED uses about + * 450µA at full brightness, which is 45 times the normal power consumption of + * the watch. The red LED is an order of magnitude less efficient (4500 µA), + * and the yellow setting lights both LEDs, which chews through nearly + * 5 milliamperes. This means that one hour of yellow blinking is likely to + * eat up between 2 and 3 percent of the battery’s usable life! + * + * Still, if you need to signal your location to someone in a dark forest, + * this watch face could come in handy. Just try to use the green LED as much + * as you can. + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/complication/breathing_face.h b/movement/watch_faces/complication/breathing_face.h index 2ff947a..fd603f1 100644 --- a/movement/watch_faces/complication/breathing_face.h +++ b/movement/watch_faces/complication/breathing_face.h @@ -25,6 +25,17 @@ #ifndef BREATHING_FACE_H_ #define BREATHING_FACE_H_ +/* + * BOXED BREATHING face + * + * Breathing is a complication for guiding boxed breathing sessions. + * Boxed breathing is a technique to help you stay calm and improve + * concentration in stressful situations. + * + * Usage: Timed messages will cycle as long as this face is active. + * Press ALARM to toggle sound. + */ + #include "movement.h" void breathing_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/movement/watch_faces/complication/couch_to_5k_face.c b/movement/watch_faces/complication/couch_to_5k_face.c new file mode 100644 index 0000000..8aa3fe6 --- /dev/null +++ b/movement/watch_faces/complication/couch_to_5k_face.c @@ -0,0 +1,267 @@ +/* + * 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 "couch_to_5k_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(couch_to_5k_state_t *state){ + return state->exercise_type == C25K_FINISHED; +} +static inline bool _cleared(couch_to_5k_state_t *state){ + return state->timer == C25K_SESSIONS[state->session][0] + && state->exercise == 0; +} +static inline void _next_session(couch_to_5k_state_t *state){ + if (++state->session >= C25K_SESSIONS_LENGTH){ + state->session = 0; + } +} + +static inline void _assign_exercise_type(couch_to_5k_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(couch_to_5k_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(couch_to_5k_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(couch_to_5k_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 couch_to_5k_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(couch_to_5k_state_t)); + memset(*context_ptr, 0, sizeof(couch_to_5k_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 couch_to_5k_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 couch_to_5k_face_loop(movement_event_t event, movement_settings_t *settings, + void *context) { + couch_to_5k_state_t *state = (couch_to_5k_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: + if (settings->bit.button_should_sound) { + watch_buzzer_play_note(BUZZER_NOTE_C8, 50); + } + 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 couch_to_5k_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/couch_to_5k_face.h b/movement/watch_faces/complication/couch_to_5k_face.h new file mode 100644 index 0000000..3c36ce6 --- /dev/null +++ b/movement/watch_faces/complication/couch_to_5k_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; +} couch_to_5k_state_t; + +void couch_to_5k_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void couch_to_5k_face_activate(movement_settings_t *settings, void *context); +bool couch_to_5k_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void couch_to_5k_face_resign(movement_settings_t *settings, void *context); + +#define couch_to_5k_face ((const watch_face_t){ \ + couch_to_5k_face_setup, \ + couch_to_5k_face_activate, \ + couch_to_5k_face_loop, \ + couch_to_5k_face_resign, \ + NULL, \ +}) + +#endif // COUCHTO5K_FACE_H_ + diff --git a/movement/watch_faces/complication/countdown_face.c b/movement/watch_faces/complication/countdown_face.c index aa23ddc..be04040 100644 --- a/movement/watch_faces/complication/countdown_face.c +++ b/movement/watch_faces/complication/countdown_face.c @@ -23,27 +23,12 @@ * SOFTWARE. */ -//----------------------------------------------------------------------------- - #include #include #include "countdown_face.h" #include "watch.h" #include "watch_utility.h" -/* - Slight extension of the original countdown face by Wesley Ellis. - - - Press the light button to enter setting mode and adjust the - countdown timer. - - - Start and pause the countdown using the alarm button, similar to the - stopwatch face. - - - When paused or terminated, press the light button to restore the - last entered countdown. -*/ - #define CD_SELECTIONS 3 #define DEFAULT_MINUTES 3 diff --git a/movement/watch_faces/complication/countdown_face.h b/movement/watch_faces/complication/countdown_face.h index 12bb1d1..1fe7c37 100644 --- a/movement/watch_faces/complication/countdown_face.h +++ b/movement/watch_faces/complication/countdown_face.h @@ -22,22 +22,27 @@ * SOFTWARE. */ -//----------------------------------------------------------------------------- - #ifndef COUNTDOWN_FACE_H_ #define COUNTDOWN_FACE_H_ -#include "movement.h" - /* -A countdown/timer face - -Max countdown is 23 hours, 59 minutes and 59 seconds. - -Note: we have to prevent the watch from going to deep sleep using -movement_schedule_background_task() while the timer is running. -*/ + * COUNTDOWN TIMER face + * + * Slight extension of the original countdown face by Wesley Ellis. + * - Press the light button to enter setting mode and adjust the + * countdown timer. + * - Start and pause the countdown using the alarm button, similar + * to the stopwatch face. + * - When paused or terminated, press the light button to restore the + * last entered countdown. + * + * Max countdown is 23 hours, 59 minutes and 59 seconds. + * + * Note: we have to prevent the watch from going to deep sleep using + * movement_schedule_background_task() while the timer is running. + */ +#include "movement.h" typedef enum { cd_paused, diff --git a/movement/watch_faces/complication/counter_face.h b/movement/watch_faces/complication/counter_face.h index 85f203e..3ac6a9b 100644 --- a/movement/watch_faces/complication/counter_face.h +++ b/movement/watch_faces/complication/counter_face.h @@ -25,9 +25,19 @@ #ifndef COUNTER_FACE_H_ #define COUNTER_FACE_H_ +/* + * COUNTER face + * + * Counter face is designed to count the number of running laps during exercises. + * + * Usage: + * Short-press ALARM to increment the counter (loops at 99) + * Long-press ALARM to reset the counter. + * Long-press LIGHT to toggle sound. + */ + #include "movement.h" -// Counter face is designed to count the number of running laps during excercises. typedef struct { uint8_t counter_idx; bool beep_on; diff --git a/movement/watch_faces/complication/databank_face.c b/movement/watch_faces/complication/databank_face.c index 9bc22da..8be54a6 100644 --- a/movement/watch_faces/complication/databank_face.c +++ b/movement/watch_faces/complication/databank_face.c @@ -20,8 +20,6 @@ * 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. - * - * Displays some pre-defined data that you might want to remember. Math constants, birthdays, phone numbers... */ #include @@ -96,12 +94,8 @@ bool databank_face_loop(movement_event_t event, movement_settings_t *settings, v case EVENT_ACTIVATE: display(); case EVENT_TICK: - // on activate and tick, if we are animating, break; case EVENT_LIGHT_BUTTON_UP: - // when the user presses 'light', we illuminate the LED. We could override this if - // our UI needed an additional button for input, consuming the light button press - // but not illuminating the LED. databank_state.current_word = (databank_state.current_word + max_words - 1) % max_words; display(); break; @@ -116,8 +110,6 @@ bool databank_face_loop(movement_event_t event, movement_settings_t *settings, v display(); break; case EVENT_ALARM_BUTTON_UP: - // when the user presses 'alarm', we toggle the state of the animation. If animating, - // we stop; if stopped, we resume. databank_state.current_word = (databank_state.current_word + 1) % max_words; display(); break; diff --git a/movement/watch_faces/complication/databank_face.h b/movement/watch_faces/complication/databank_face.h index 1f204b2..3376cef 100644 --- a/movement/watch_faces/complication/databank_face.h +++ b/movement/watch_faces/complication/databank_face.h @@ -25,6 +25,23 @@ #ifndef DATABANK_FACE_H_ #define DATABANK_FACE_H_ +/* + * DATABANK face + * + * Displays some pre-defined data that you might want to remember + * Math constants, birthdays, phone numbers... + * + * Usage: Edit the global variable `pi_data` in "databank_face.c" + * to the define the data that will be displayed. Each "item" contains + * a two-letter label (using the day-of-week display), then a longer + * string that will be displayed one "word" (six characters) at a time. + * + * Short-press ALARM to display the next word. + * Short-press LIGHT to display the previous word. + * Long-press ALARM to display the next item. + * Long-press LIGHT to display the previous item. + */ + #include "movement.h" void databank_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/movement/watch_faces/complication/day_one_face.c b/movement/watch_faces/complication/day_one_face.c index 25ce1c2..27601ed 100644 --- a/movement/watch_faces/complication/day_one_face.c +++ b/movement/watch_faces/complication/day_one_face.c @@ -27,24 +27,54 @@ #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; } -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); } +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; @@ -54,7 +84,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,11 +98,9 @@ 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; - // reset the current page to 0, display days alive. - state->current_page = 0; + 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); @@ -85,96 +113,143 @@ 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]; + char buf[9]; 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->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 1: - watch_display_string("YR ", 0); - if (event.subsecond % 2) { - sprintf(buf, "%4d", state->birth_year); - watch_display_string(buf, 4); - } - break; - case 2: - watch_display_string("MO ", 0); - if (event.subsecond % 2) { - sprintf(buf, "%2d", state->birth_month); - watch_display_string(buf, 4); - } - break; - case 3: - watch_display_string("DA ", 0); - if (event.subsecond % 2) { - sprintf(buf, "%2d", state->birth_day); - watch_display_string(buf, 6); - } - 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 == 0) 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 != 0) { - // 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); - } + 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 != 0) { - state->birthday_changed = true; - switch (state->current_page) { - case 1: - state->birth_year = state->birth_year + 1; - if (state->birth_year > state->current_year) state->birth_year = 1900; - break; - case 2: - state->birth_month = (state->birth_month % 12) + 1; - break; - case 3: - 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; - } + 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 == 0) { - state->current_page++; - movement_request_tick_frequency(4); + 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: + _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 != 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 ab8372b..9a59e3b 100644 --- a/movement/watch_faces/complication/day_one_face.h +++ b/movement/watch_faces/complication/day_one_face.h @@ -25,18 +25,46 @@ #ifndef DAY_ONE_FACE_H_ #define DAY_ONE_FACE_H_ +/* + * DAY ONE face + * + * This watch face displays the number of days since or until a given date. + * It was originally designed to display the number of days you’ve been alive, + * but technically it can count up from any date in the 20th century or the + * 21st century, so far. + * + * Long press on the Alarm button to enter customization mode. The text “YR” + * will appear, and will allow you to set the year starting from 1959. Press + * Alarm repeatedly to advance the year. If your birthday is before 1959, + * advance beyond the current year and it will wrap around to 1900. + * + * Once you have set the year, press Light to set the month (“MO”) and + * day (“DA”), advancing the value by pressing Alarm repeatedly. + * + * Note that at this time, the Day One face does not display the sleep + * indicator in sleep mode, which may make the watch appear to be + * unresponsive in sleep mode. You can still press the Alarm button to + * wake the watch. This UI quirk will be addressed in a future update. + */ + #include "movement.h" -// The Day One face is designed to count upwards from the wearer's date of birth. 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, + PAGE_DATE +} day_one_page_t; typedef struct { - uint8_t current_page; - uint16_t current_year; + day_one_page_t current_page; uint16_t birth_year; uint8_t birth_month; 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); diff --git a/movement/watch_faces/complication/discgolf_face.c b/movement/watch_faces/complication/discgolf_face.c index 0852bf1..7b5142a 100644 --- a/movement/watch_faces/complication/discgolf_face.c +++ b/movement/watch_faces/complication/discgolf_face.c @@ -1,7 +1,7 @@ #include #include #include "discgolf_face.h" -#include "watch.h" // Remember to change number of courses in this file +#include "watch.h" // Remember to change number of courses in this file #include "watch_utility.h" /* diff --git a/movement/watch_faces/complication/discgolf_face.h b/movement/watch_faces/complication/discgolf_face.h index 5e8068e..d168958 100644 --- a/movement/watch_faces/complication/discgolf_face.h +++ b/movement/watch_faces/complication/discgolf_face.h @@ -22,21 +22,24 @@ * SOFTWARE. */ -///////////////////////////////////////////////////////////////////////////////////// +#ifndef DISCGOLF_FACE_H_ +#define DISCGOLF_FACE_H_ -/* +/* + * DISC GOLF face + * * Keep track of scores in discgolf or golf! * The watch face operates in three different modes: * * - dg_setting: Select a course * Enter this mode by holding down the light button. The screen will display - * the label for the hole and the lowest score since last boot. - * Press alarm to loop through the holes. Press the light button to make a + * the label for the hole and the lowest score since last boot. + * Press alarm to loop through the holes. Press the light button to make a * selection. This will reset all scores and start a new game in dg_idle mode. * * -dg_idle: We're playing a hole * This either shows your current score relative to par, or the score for a - * particular hole. + * particular hole. * At the start of a game, press alarm to loop through the holes and leave it * your starting hole. For optimal experience, play the course linearly after that * If you're viewing the hole you're supposed to be playing, the watch face will @@ -49,19 +52,15 @@ * -dg_scoring: Input score for a hole * In this mode, if the score is 0 (hasn't been entered during this round), * it will blink, indicating we're in scoring mode. Press the alarm button - * to increment the score up until 15, in which case it loops back to 0. + * to increment the score up until 15, in which case it loops back to 0. * Press the light button to save the score for that hole, advance one hole * if you're not editing an already input score, and returning to idle mode. * - * When all scores have been entered, the LAP indicator turns on. At that point, if we enter - * dg_setting to select a course, the score for that round is evaluated against the current + * When all scores have been entered, the LAP indicator turns on. At that point, if we enter + * dg_setting to select a course, the score for that round is evaluated against the current * lowest score for that course, and saved if it is better. */ - -#ifndef DISCGOLF_FACE_H_ -#define DISCGOLF_FACE_H_ - #include "movement.h" #define courses 11 @@ -75,7 +74,7 @@ typedef struct { uint8_t course; // Index for course selection, from 0 uint8_t hole; // Index for current hole, from 1 uint8_t playing; // Current hole - int scores[18]; // Scores for each played hole + int scores[18]; // Scores for each played hole discgolf_mode_t mode; // Watch face mode } discgolf_state_t; diff --git a/movement/watch_faces/complication/dual_timer_face.h b/movement/watch_faces/complication/dual_timer_face.h index d7c6cfa..d1ac793 100644 --- a/movement/watch_faces/complication/dual_timer_face.h +++ b/movement/watch_faces/complication/dual_timer_face.h @@ -26,16 +26,6 @@ #ifndef DUAL_TIMER_FACE_H_ #define DUAL_TIMER_FACE_H_ -#include "movement.h" - -/* - * IMPORTANT: This watch face uses the same TC2 callback counter as the Stock Stopwatch - * watch-face. It works through calling a global handler function. The two watch-faces - * therefore can't coexist within the same firmware. If you want to compile this watch-face - * then you need to remove the line <../watch_faces/complication/stock_stopwatch_face.c \> - * from the Makefile. - */ - /* * DUAL TIMER * ========== @@ -70,8 +60,15 @@ * the timers. In this case LONG PRESSING MODE will move to the next face instead of moving * back to the default watch face. * + * IMPORTANT: This watch face uses the same TC2 callback counter as the Stock Stopwatch + * watch-face. It works through calling a global handler function. The two watch-faces + * therefore can't coexist within the same firmware. If you want to compile this watch-face + * then you need to remove the line <../watch_faces/complication/stock_stopwatch_face.c \> + * from the Makefile. */ +#include "movement.h" + typedef struct { uint8_t centiseconds : 7; // 0-59 uint8_t seconds : 6; // 0-59 diff --git a/movement/watch_faces/complication/flashlight_face.h b/movement/watch_faces/complication/flashlight_face.h index 2675015..8c0ef8d 100644 --- a/movement/watch_faces/complication/flashlight_face.h +++ b/movement/watch_faces/complication/flashlight_face.h @@ -25,9 +25,9 @@ #ifndef FLASHLIGHT_FACE_H_ #define FLASHLIGHT_FACE_H_ -#include "movement.h" - /* + * FLASHLIGHT face + * * A flashlight for use with the Flashlight sensor board. * * When the watch face appears, the display will show "FL" in the top two positions. @@ -35,6 +35,8 @@ * */ +#include "movement.h" + typedef struct { // Anything you need to keep track of, put it here! uint8_t unused; diff --git a/movement/watch_faces/complication/geomancy_face.h b/movement/watch_faces/complication/geomancy_face.h index 4a19ba8..4710ae7 100644 --- a/movement/watch_faces/complication/geomancy_face.h +++ b/movement/watch_faces/complication/geomancy_face.h @@ -25,10 +25,8 @@ #ifndef GEOMANCY_FACE_H_ #define GEOMANCY_FACE_H_ -#include "movement.h" - /* - * GEOMANCY WATCH FACE + * GEOMANCY watch face * * A simple and straightforward watch face for the ancient Eastern geomantic divination system * of I Ching and the western system of "Geomancy". It is an optional addition to the Toss Up @@ -65,6 +63,8 @@ * */ +#include "movement.h" + typedef struct { uint8_t bits : 4; } nibble_t; diff --git a/movement/watch_faces/complication/habit_face.h b/movement/watch_faces/complication/habit_face.h index 80a4884..d4f43cf 100644 --- a/movement/watch_faces/complication/habit_face.h +++ b/movement/watch_faces/complication/habit_face.h @@ -25,8 +25,6 @@ #ifndef HABIT_FACE_H_ #define HABIT_FACE_H_ -#include "movement.h" - /* * Habit tracking face * @@ -36,6 +34,8 @@ * */ +#include "movement.h" + void habit_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr); void habit_face_activate(movement_settings_t *settings, void *context); diff --git a/movement/watch_faces/complication/interval_face.c b/movement/watch_faces/complication/interval_face.c index dabc6b1..f498323 100644 --- a/movement/watch_faces/complication/interval_face.c +++ b/movement/watch_faces/complication/interval_face.c @@ -22,8 +22,6 @@ * SOFTWARE. */ -//----------------------------------------------------------------------------- - #include #include @@ -33,57 +31,6 @@ #include "watch_private_display.h" #include "watch_buzzer.h" -/* - This face brings 9 customizable interval timers to the sensor watch, - to be used as hiit training device and/or for time management techniques. - - - There are 9 interval timer slots, you can cycle through these with the - alarm button (short press). For each timer slot, a short "slideshow" - displaying the relevant details (like length of each phase - see below) - is shown. - - - To start an interval timer, press and hold the alarm button. - - - To pause a running timer, press the alarm button (short press). - - - To completely abort a running timer, press and hold the alarm button. - - - Press and hold the light button to enter settings mode for each interval - timer slot. - - - Each interval timer has 1 to 4 phases of customizable length like so: - (1) prepare/warum up --> (2) work --> (3) break --> (4) cool down. - When setting up or running a timer, each of these phases is displayed by - the letters "PR" (prepare), "WO" (work), "BR" (break), "CD" (cool down). - - - Each of these phases is optional, you can set the corresponding - minutes and seconds to zero. But at least one phase needs to be set, if - you want to use the timer. - - - You can define the number of rounds either only for the work - phase and/or for the combination of work + break phase. Let's say you - want an interval timer that counts 3 rounds of 30 seconds work, - followed by 20 seconds rest: - work 30s --> work 30s --> work 30s --> break 20s - You can do this by setting 30s for the "WO"rk phase and setting a 3 - in the lower right hand corner of the work page. The "LAP" indicator - lights up at this position, to explain that we are setting laps here. - After that, set the "BR"eak phase to 20s and leave the rest as it is. - - - If you want to set up a certain number of "full rounds", consisting - of work phase(s) plus breaks, you can do so at the "BR"eak page. The - number in the lower right hand corner determines the number of full - rounds to be counted. A "-" means, that there is no limit and the - timer keeps alternating between work and break phases. - - - This watch face comes with several pre-defined interval timers, - suitable for hiit training (timer slots 1 to 4) as well as doing - work according to the pomodoro principle (timer slots 5 to 6). - Feel free to adjust the timer slots to your own needs (or completely - wipe them ;-) - -*/ - typedef enum { interval_setting_0_timer_idx, interval_setting_1_clear_yn, diff --git a/movement/watch_faces/complication/interval_face.h b/movement/watch_faces/complication/interval_face.h index fa0a428..7997585 100644 --- a/movement/watch_faces/complication/interval_face.h +++ b/movement/watch_faces/complication/interval_face.h @@ -22,16 +22,62 @@ * SOFTWARE. */ -//----------------------------------------------------------------------------- - #ifndef INTERVAL_FACE_H_ #define INTERVAL_FACE_H_ -#include "movement.h" - /* -A face for customizable interval timers -*/ + * INTERVAL TIMER face + * + * This face brings 9 customizable interval timers to the sensor watch, + * to be used as hiit training device and/or for time management techniques. + * + * - There are 9 interval timer slots, you can cycle through these with the + * alarm button (short press). For each timer slot, a short "slideshow" + * displaying the relevant details (like length of each phase - see below) + * is shown. + * + * - To start an interval timer, press and hold the alarm button. + * + * - To pause a running timer, press the alarm button (short press). + * + * - To completely abort a running timer, press and hold the alarm button. + * + * - Press and hold the light button to enter settings mode for each interval + * timer slot. + * + * - Each interval timer has 1 to 4 phases of customizable length like so: + * (1) prepare/warum up --> (2) work --> (3) break --> (4) cool down. + * When setting up or running a timer, each of these phases is displayed by + * the letters "PR" (prepare), "WO" (work), "BR" (break), "CD" (cool down). + * + * - Each of these phases is optional, you can set the corresponding + * minutes and seconds to zero. But at least one phase needs to be set, if + * you want to use the timer. + * + * - You can define the number of rounds either only for the work + * phase and/or for the combination of work + break phase. Let's say you + * want an interval timer that counts 3 rounds of 30 seconds work, + * followed by 20 seconds rest: + * work 30s --> work 30s --> work 30s --> break 20s + * You can do this by setting 30s for the "WO"rk phase and setting a 3 + * in the lower right hand corner of the work page. The "LAP" indicator + * lights up at this position, to explain that we are setting laps here. + * After that, set the "BR"eak phase to 20s and leave the rest as it is. + * + * - If you want to set up a certain number of "full rounds", consisting + * of work phase(s) plus breaks, you can do so at the "BR"eak page. The + * number in the lower right hand corner determines the number of full + * rounds to be counted. A "-" means, that there is no limit and the + * timer keeps alternating between work and break phases. + * + * - This watch face comes with several pre-defined interval timers, + * suitable for hiit training (timer slots 1 to 4) as well as doing + * work according to the pomodoro principle (timer slots 5 to 6). + * Feel free to adjust the timer slots to your own needs (or completely + * wipe them ;-) + */ + +#include "movement.h" #define INTERVAL_TIMERS 9 // no of available customizable timers (be aware: only 4 bits reserved for this value in struct below) diff --git a/movement/watch_faces/complication/invaders_face.h b/movement/watch_faces/complication/invaders_face.h index 59126dd..37e9188 100644 --- a/movement/watch_faces/complication/invaders_face.h +++ b/movement/watch_faces/complication/invaders_face.h @@ -25,8 +25,6 @@ #ifndef INVADERS_FACE_H_ #define INVADERS_FACE_H_ -#include "movement.h" - /* * Remake of the "famous" Casio Number Invaders Game * @@ -60,6 +58,8 @@ * */ +#include "movement.h" + typedef struct { uint16_t highscore; bool sound_on; diff --git a/movement/watch_faces/complication/kitchen_conversions_face.c b/movement/watch_faces/complication/kitchen_conversions_face.c new file mode 100644 index 0000000..c19e755 --- /dev/null +++ b/movement/watch_faces/complication/kitchen_conversions_face.c @@ -0,0 +1,480 @@ +/* + * MIT License + * + * Copyright (c) 2023 PrimmR + * + * 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 "kitchen_conversions_face.h" + +typedef struct +{ + char name[6]; // Name to display on selection + double conv_factor_uk; // Unit as represented in base units (UK) + double conv_factor_us; // Unit as represented in base units (US) + int16_t linear_factor; // Addition of constant (For temperatures) +} unit; + +#define TICK_FREQ 4 + +#define MEASURES_COUNT 3 // Number of different measurement 'types' +#define WEIGHT 0 +#define TEMP 1 +#define VOL 2 + +// Names of measurements +static char measures[MEASURES_COUNT][6] = {"WeIght", " Temp", " VOL"}; + +// Number of items in each category +#define WEIGHT_COUNT 4 +#define TEMP_COUNT 3 +#define VOL_COUNT 9 +const uint8_t units_count[4] = {WEIGHT_COUNT, TEMP_COUNT, VOL_COUNT}; + +static const unit weights[WEIGHT_COUNT] = { + {" g", 1., 1., 0}, // BASE + {" kg", 1000., 1000, 0}, + {"Ounce", 28.34952, 28.34952, 0}, + {" Pound", 453.5924, 453.5924, 0}, +}; + +static const unit temps[TEMP_COUNT] = { + {" # C", 1.8, 1.8, 32}, + {" # F", 1., 1., 0}, // BASE + {"Gas Mk", 25., 25., 250}, +}; + +static const unit vols[VOL_COUNT] = { + {" n&L", 1., 1., 0}, // BASE (ml) + {" L", 1000., 1000., 0}, + {" Fl Oz", 28.41306, 29.57353, 0}, + {" Tbsp", 17.75816, 14.78677, 0}, + {" Tsp", 5.919388, 4.928922, 0}, + {" Cup", 284.1306, 236.5882, 0}, + {" Pint", 568.2612, 473.1765, 0}, + {" Quart", 1136.522, 946.353, 0}, + {"Gallon", 4546.09, 3785.412, 0}, +}; + +static int8_t calc_success_seq[5] = {BUZZER_NOTE_G6, 10, BUZZER_NOTE_C7, 10, 0}; +static int8_t calc_fail_seq[5] = {BUZZER_NOTE_C7, 10, BUZZER_NOTE_G6, 10, 0}; + +// Resets all state variables to 0 +static void reset_state(kitchen_conversions_state_t *state, movement_settings_t *settings) +{ + state->pg = measurement; + state->measurement_i = 0; + state->from_i = 0; + state->from_is_us = settings->bit.use_imperial_units; // If uses imperial, most likely to be US + state->to_i = 0; + state->to_is_us = settings->bit.use_imperial_units; + state->selection_value = 0; + state->selection_index = 0; + state->light_held = false; +} + +void kitchen_conversions_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(kitchen_conversions_state_t)); + memset(*context_ptr, 0, sizeof(kitchen_conversions_state_t)); + // Do any one-time tasks in here; the inside of this conditional happens only at boot. + } + // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. +} + +void kitchen_conversions_face_activate(movement_settings_t *settings, void *context) +{ + (void)settings; + kitchen_conversions_state_t *state = (kitchen_conversions_state_t *)context; + + // Handle any tasks related to your watch face coming on screen. + movement_request_tick_frequency(TICK_FREQ); + + reset_state(state, settings); +} + +// Increments index pointer by 1, wrapping +#define increment_wrapping(index, wrap) ({(index)++; index %= wrap; }) + +static uint32_t pow_10(uint8_t n) +{ + uint32_t result = 1; + for (int i = 0; i < n; i++) + { + result *= 10; + } + return result; +} + +// Returns correct list of units for the measurement index +static unit *get_unit_list(uint8_t measurement_i) +{ + switch (measurement_i) + { + case WEIGHT: + return (unit *)weights; + + case TEMP: + return (unit *)temps; + + case VOL: + return (unit *)vols; + + default: + return (unit *)weights; + } +} + +// Increment digit by 1 in input (wraps) +static void increment_input(kitchen_conversions_state_t *state) +{ + uint8_t digit = state->selection_value / pow_10(DISPLAY_DIGITS - 1 - state->selection_index) % 10; + if (digit != 9) + { + state->selection_value += pow_10(DISPLAY_DIGITS - 1 - state->selection_index); + } + else + { + state->selection_value -= 9 * pow_10(DISPLAY_DIGITS - 1 - state->selection_index); + } +} + +// Displays the list of units in the selected category +static void display_units(uint8_t measurement_i, uint8_t list_i) +{ + watch_display_string(get_unit_list(measurement_i)[list_i].name, 4); +} + +static void display(kitchen_conversions_state_t *state, movement_settings_t *settings, uint8_t subsec) +{ + watch_clear_display(); + + switch (state->pg) + { + case measurement: + { + watch_display_string("Un", 0); + watch_display_string(measures[state->measurement_i], 4); + } + break; + + case from: + display_units(state->measurement_i, state->from_i); + + // Display Fr if non-locale specific, else display locale and F + if (state->measurement_i == VOL) + { + watch_display_string("F", 3); + + char *locale = state->from_is_us ? "A " : "GB"; + watch_display_string(locale, 0); + } + else + { + watch_display_string("Fr", 0); + } + + break; + + case to: + display_units(state->measurement_i, state->to_i); + + // Display To if non-locale specific, else display locale and T + if (state->measurement_i == VOL) + { + watch_display_string("T", 3); + + char *locale = state->to_is_us ? "A " : "GB"; + watch_display_string(locale, 0); + } + else + { + watch_display_string("To", 0); + } + + break; + + case input: + { + char buf[7]; + sprintf(buf, "%06lu", state->selection_value); + watch_display_string(buf, 4); + + // Only allow ints for Gas Mk + if (state->measurement_i == TEMP && state->from_i == 2) + { + watch_display_string(" ", 8); + } + + // Blink digit (on & off) twice a second + if (subsec % 2) + { + watch_display_string(" ", 4 + state->selection_index); + } + + watch_display_string("In", 0); + } + break; + + case result: + { + unit froms = get_unit_list(state->measurement_i)[state->from_i]; + unit tos = get_unit_list(state->measurement_i)[state->to_i]; + // Chooses correct factor for locale + double f_conv_factor = state->from_is_us ? froms.conv_factor_us : froms.conv_factor_uk; + double t_conv_factor = state->to_is_us ? tos.conv_factor_us : tos.conv_factor_uk; + // Converts + double to_base = (state->selection_value * f_conv_factor) + 100 * froms.linear_factor; + double conversion = ((to_base - 100 * tos.linear_factor) / t_conv_factor); + + // If number too large or too small + uint8_t lower_bound = (state->measurement_i == TEMP && state->to_i == 2) ? 100 : 0; + if (conversion >= 1000000 || conversion < lower_bound) + { + watch_set_indicator(WATCH_INDICATOR_BELL); + watch_display_string("Err", 5); + + if (settings->bit.button_should_sound) + watch_buzzer_play_sequence(calc_fail_seq, NULL); + } + else + { + uint32_t rounded = conversion + .5; + char buf[7]; + sprintf(buf, "%6lu", rounded); + watch_display_string(buf, 4); + + // Make sure LSDs always filled + if (rounded < 10) + { + watch_display_string("00", 7); + } + else if (rounded < 100) + { + watch_display_string("0", 7); + } + + if (settings->bit.button_should_sound) + watch_buzzer_play_sequence(calc_success_seq, NULL); + } + watch_display_string("=", 1); + } + + break; + + default: + break; + } +} + +bool kitchen_conversions_face_loop(movement_event_t event, movement_settings_t *settings, void *context) +{ + kitchen_conversions_state_t *state = (kitchen_conversions_state_t *)context; + + switch (event.event_type) + { + case EVENT_ACTIVATE: + // Initial UI + display(state, settings, event.subsecond); + break; + case EVENT_TICK: + // Update for blink animation on input + if (state->pg == input) + { + display(state, settings, event.subsecond); + + // Increments input twice a second when light button held + if (state->light_held && event.subsecond % 2) + increment_input(state); + } + break; + case EVENT_LIGHT_BUTTON_UP: + // Cycles options + switch (state->pg) + { + case measurement: + increment_wrapping(state->measurement_i, MEASURES_COUNT); + break; + + case from: + increment_wrapping(state->from_i, units_count[state->measurement_i]); + break; + + case to: + increment_wrapping(state->to_i, units_count[state->measurement_i]); + break; + + case input: + increment_input(state); + break; + + default: + break; + } + + // Light button does nothing on final screen + if (state->pg != result) + display(state, settings, event.subsecond); + + state->light_held = false; + + break; + + case EVENT_ALARM_BUTTON_UP: + // Increments selected digit + if (state->pg == input) + { + + // Moves between digits in input + // Wraps at 6 digits unless gas mark selected + if (state->selection_index < (DISPLAY_DIGITS - 1) - 2 * (state->measurement_i == TEMP && state->from_i == 2)) + { + state->selection_index++; + } + else + { + state->pg++; + display(state, settings, event.subsecond); + } + } + // Moves forward 1 page + else + { + if (state->pg == SCREEN_NUM - 1) + { + reset_state(state, settings); + } + else + { + state->pg++; + } + + // Play boop + if (settings->bit.button_should_sound) + watch_buzzer_play_note(BUZZER_NOTE_C7, 50); + } + + display(state, settings, event.subsecond); + + state->light_held = false; + + break; + + case EVENT_ALARM_LONG_PRESS: + // Moves backwards through pages, resetting certain values + if (state->pg != measurement) + { + switch (state->pg) + { + case measurement: + state->measurement_i = 0; + break; + + case from: + state->from_i = 0; + state->from_is_us = settings->bit.use_imperial_units; + break; + + case to: + state->to_i = 0; + state->to_is_us = settings->bit.use_imperial_units; + break; + + case input: + state->selection_index = 0; + state->selection_value = 0; + break; + + case result: + state->selection_index = 0; + break; + + default: + break; + } + + state->pg--; + display(state, settings, event.subsecond); + + // Play beep + if (settings->bit.button_should_sound) + watch_buzzer_play_note(BUZZER_NOTE_C8, 50); + + state->light_held = false; + } + break; + + case EVENT_LIGHT_LONG_PRESS: + // Switch between locales + if (state->measurement_i == VOL) + { + if (state->pg == from) + { + state->from_is_us = !state->from_is_us; + } + else if (state->pg == to) + { + state->to_is_us = !state->to_is_us; + } + + if (state->pg == from || state->pg == to) + { + display(state, settings, event.subsecond); + + // Play bleep + if (settings->bit.button_should_sound) + watch_buzzer_play_note(BUZZER_NOTE_E7, 50); + } + } + + // Sets flag to increment input digit when light held + if (state->pg == input) + state->light_held = true; + + break; + + case EVENT_LIGHT_LONG_UP: + state->light_held = false; + break; + + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void kitchen_conversions_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/kitchen_conversions_face.h b/movement/watch_faces/complication/kitchen_conversions_face.h new file mode 100644 index 0000000..e732579 --- /dev/null +++ b/movement/watch_faces/complication/kitchen_conversions_face.h @@ -0,0 +1,87 @@ +/* + * MIT License + * + * Copyright (c) 2023 PrimmR + * + * 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 KITCHEN_CONVERSIONS_FACE_H_ +#define KITCHEN_CONVERSIONS_FACE_H_ + +#include "movement.h" + +/* + * Kitchen Conversions + * A face that allows the user to convert between common kitchen units of measurement + * + * How to use + * ---------- + * Short press the alarm button to move forward through menus, and long press to move backwards + * + * Press the light button to cycle through options in the menus + * + * When inputting a number, the light button moves forward one place and the alarm button increments a digit by one + * + * To convert between Imperial (GB) and US (A) measurements of volume, hold the light button + * + */ + +#define SCREEN_NUM 5 + +// Names of each page +typedef enum +{ + measurement, + from, + to, + input, + result, +} page_t; + +#define DISPLAY_DIGITS 6 + +// Settings when app is running +typedef struct +{ + page_t pg; + uint8_t measurement_i; + uint8_t from_i; + bool from_is_us; + uint8_t to_i; + bool to_is_us; + uint32_t selection_value; + uint8_t selection_index; + bool light_held; +} kitchen_conversions_state_t; + +void kitchen_conversions_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr); +void kitchen_conversions_face_activate(movement_settings_t *settings, void *context); +bool kitchen_conversions_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void kitchen_conversions_face_resign(movement_settings_t *settings, void *context); + +#define kitchen_conversions_face ((const watch_face_t){ \ + kitchen_conversions_face_setup, \ + kitchen_conversions_face_activate, \ + kitchen_conversions_face_loop, \ + kitchen_conversions_face_resign, \ + NULL, \ +}) + +#endif // KITCHEN_CONVERSIONS_FACE_H_ diff --git a/movement/watch_faces/complication/moon_phase_face.c b/movement/watch_faces/complication/moon_phase_face.c index 9aac374..f74de64 100644 --- a/movement/watch_faces/complication/moon_phase_face.c +++ b/movement/watch_faces/complication/moon_phase_face.c @@ -161,6 +161,10 @@ bool moon_phase_face_loop(movement_event_t event, movement_settings_t *settings, state->offset += 86400; _update(settings, state, state->offset); break; + case EVENT_ALARM_LONG_PRESS: + state->offset = 0; + _update(settings, state, state->offset); + break; case EVENT_TIMEOUT: // QUESTION: Should timeout reset offset to 0? break; diff --git a/movement/watch_faces/complication/moon_phase_face.h b/movement/watch_faces/complication/moon_phase_face.h index 35d6318..9a64fcc 100644 --- a/movement/watch_faces/complication/moon_phase_face.h +++ b/movement/watch_faces/complication/moon_phase_face.h @@ -25,6 +25,30 @@ #ifndef MOON_PHASE_FACE_H_ #define MOON_PHASE_FACE_H_ +/* + * MOON PHASE face + * + * The Moon Phase face is similar to the Sunrise/Sunset face: it displays the + * current phase of the moon, along with the day of the month and a graphical + * representation of the moon on the top row. + * + * This graphical representation is a bit abstract. The segments that turn on + * represent the shape of the moon, waxing from the bottom right and waning at + * the top left. A small crescent at the bottom right will grow into a larger + * crescent, then add lines in the center for a quarter and half moon. All + * segments are on during a full moon. Then gradually the segments at the + * bottom right will turn off, until all that remains is a small waning + * crescent at the top left. + * + * All segments turn off during a new moon. + * + * On this screen you may press the Alarm button repeatedly to move forward + * in time: the day of the month at the top right will advance by one day for + * each button press, and both the text and the graphical representation will + * display the moon phase for that day. Try pressing the Alarm button 27 times + * now, just to visualize what the moon will look like over the next month. + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/complication/morsecalc_face.c b/movement/watch_faces/complication/morsecalc_face.c index ca03a1a..30118e2 100644 --- a/movement/watch_faces/complication/morsecalc_face.c +++ b/movement/watch_faces/complication/morsecalc_face.c @@ -22,89 +22,6 @@ * SOFTWARE. */ -/* -## Morse-code-based RPN calculator - -The calculator is operated by first composing a **token** in Morse code, then submitting it to the calculator. A token specifies either a calculator operation or a float value. -These two parts of the codebase are totally independent: - - 1. The Morse-code reader (`mc.h`, `mc.c`) - 2. The RPN calculator (`calc.h`, `calc.c`, `calc_fn.h`, `calc_fn.c`, `small_strtod.c`) - -The user interface (`morsecalc_face.h`, `morsecalc_face.c`) lets you talk to the RPN calculator through Morse code. - -## Controls - - - `light` is dash - - `alarm` is dot - - `mode` is "finish character" - - long-press `mode` or submit a blank token to switch faces - - long-press `alarm` to show stack - - long-press `light` to toggle the light - -## Morse code token entry -As you enter `.`s and `-`s, the morse code char you've entered will appear in the top center digit. -At the top right is the # of morse code `.`/`-` you've input so far. The character resets at the 6th `.`/`-`. -Once you have the character you want to enter, push `mode` to enter it. -The character will be appended to the current token, whose 6 trailing chars are shown on the main display. -Once you've typed in the token you want, enter a blank Morse code character and then push `mode`. -This submits it to the calculator. - -Special characters: - - - Backspace is `(` (`-.--.`). - - Clear token input without submitting to calculator is `Start transmission` (`-.-.-`). - -## Writing commands -First the calculator will try to interpret the token as a command/stack operation. -Commands are defined in `calc_dict[]` in `movement/lib/morsecalc/calc_fns.h`. -If the command doesn't appear in the dictionary, the calculator tries to interpret the token as a number. - -## Writing numbers -Numbers are written like floating point strings. -Entering a number pushes it to the top of the stack if there's room. -This can get long, so for convenience numerals can also be written in binary with .- = 01. - - 0 1 2 3 4 5 6 7 8 9 - . - -. -- -.. -.- --. --- -... -..- - e t n m d k g o b x - - - Exponent signs must be entered as "p". - - Decimal place "." can be entered as "h" (code ....) - - Sign "-" can be entered as "Ch digraph" (code ----) - -For example: "4.2e-3" can be entered directly, or as "4h2pC3" - similarly, "0.0042" can also be entered as "eheedn" -Once you submit a number to the watch face, it pushes it to the top of the stack if there's room. - -## Number display -After a command runs, the top of the stack is displayed in this format: - - - Main 4 digits = leading 4 digits - - Last 2 digits = exponent - - Top middle = [Stack location, Sign of number] - - Top right = [Stack exponent, Sign of exponent] - -Blank sign digit means positive. -So for example, the watch face might look like this: - - [ 0 -5] - [4200 03] - -... representing `+4.200e-3` is in stack location 0 (the top) and it's one of five items in the stack. - -## Looking at the stack -To show the top of the stack, push and hold `light`/`alarm` or submit a blank token by pushing `mode` a bunch of times. -To show the N-th stack item (0 through 9): - - - Put in the Morse code for N without pushing the mode button. - - Push and hold `alarm`. - -To show the memory register, use `m` instead of a number. - -To see all the calculator operations and their token aliases, see the `calc_dict[]` struct in `calc_fns.h` -*/ - #include #include #include diff --git a/movement/watch_faces/complication/morsecalc_face.h b/movement/watch_faces/complication/morsecalc_face.h index 2ee1862..4768cce 100644 --- a/movement/watch_faces/complication/morsecalc_face.h +++ b/movement/watch_faces/complication/morsecalc_face.h @@ -25,6 +25,96 @@ #ifndef MORSECALC_FACE_H_ #define MORSECALC_FACE_H_ +/* + * MORSECALC face + * Morse-code-based RPN calculator + * + * The calculator is operated by first composing a **token** in Morse code, + * then submitting it to the calculator. A token specifies either a calculator + * operation or a float value. + * + * These two parts of the codebase are totally independent: + * 1. The Morse-code reader (`mc.h`, `mc.c`) + * 2. The RPN calculator (`calc.h`, `calc.c`, `calc_fn.h`, `calc_fn.c`, `small_strtod.c`) + * + * The user interface (`morsecalc_face.h`, `morsecalc_face.c`) lets you talk + * to the RPN calculator through Morse code. + * + * ## Controls + * - `light` is dash + * - `alarm` is dot + * - `mode` is "finish character" + * - long-press `mode` or submit a blank token to switch faces + * - long-press `alarm` to show stack + * - long-press `light` to toggle the light + * + * ## Morse code token entry + * As you enter `.`s and `-`s, the morse code char you've entered will + * appear in the top center digit. At the top right is the # of morse code + * `.`/`-` you've input so far. The character resets at the 6th `.`/`-`. + * + * Once you have the character you want to enter, push `mode` to enter it. + * + * The character will be appended to the current token, whose 6 trailing + * chars are shown on the main display. Once you've typed in the token you + * want, enter a blank Morse code character and then push `mode`. + * This submits it to the calculator. + * + * Special characters: + * - Backspace is `(` (`-.--.`). + * - Clear token input without submitting to calculator is `Start + * transmission` (`-.-.-`). + * + * ## Writing commands + * First the calculator will try to interpret the token as a command/stack operation. + * Commands are defined in `calc_dict[]` in `movement/lib/morsecalc/calc_fns.h`. + * If the command doesn't appear in the dictionary, the calculator tries to interpret the token as a number. + * + * ## Writing numbers + * Numbers are written like floating point strings. + * Entering a number pushes it to the top of the stack if there's room. + * This can get long, so for convenience numerals can also be written in binary with .- = 01. + * + * 0 1 2 3 4 5 6 7 8 9 + * . - -. -- -.. -.- --. --- -... -..- + * e t n m d k g o b x + * + * - Exponent signs must be entered as "p". + * - Decimal place "." can be entered as "h" (code ....) + * - Sign "-" can be entered as "Ch digraph" (code ----) + * + * For example: "4.2e-3" can be entered directly, or as "4h2pC3" + * similarly, "0.0042" can also be entered as "eheedn" + * Once you submit a number to the watch face, it pushes it to the top of the stack if there's room. + * + * ## Number display + * After a command runs, the top of the stack is displayed in this format: + * + * - Main 4 digits = leading 4 digits + * - Last 2 digits = exponent + * - Top middle = [Stack location, Sign of number] + * - Top right = [Stack exponent, Sign of exponent] + * + * Blank sign digit means positive. + * So for example, the watch face might look like this: + * + * [ 0 -5] + * [4200 03] + * + * ... representing `+4.200e-3` is in stack location 0 (the top) and it's one of five items in the stack. + * + * ## Looking at the stack + * To show the top of the stack, push and hold `light`/`alarm` or submit a blank token by pushing `mode` a bunch of times. + * To show the N-th stack item (0 through 9): + * + * - Put in the Morse code for N without pushing the mode button. + * - Push and hold `alarm`. + * + * To show the memory register, use `m` instead of a number. + * + * To see all the calculator operations and their token aliases, see the `calc_dict[]` struct in `calc_fns.h` + */ + #define MORSECALC_TOKEN_LEN 32 #define MORSECODE_LEN 5 @@ -34,7 +124,7 @@ /* * MC International Morse Code binary tree * Levels of the tree are concatenated. - * '.' = 0 and '-' = 1. + * '.' = 0 and '-' = 1. * * Capitals denote special characters: * C = Ch digraph diff --git a/movement/watch_faces/complication/orrery_face.c b/movement/watch_faces/complication/orrery_face.c index b533960..42fdf81 100644 --- a/movement/watch_faces/complication/orrery_face.c +++ b/movement/watch_faces/complication/orrery_face.c @@ -20,7 +20,6 @@ * 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 diff --git a/movement/watch_faces/complication/orrery_face.h b/movement/watch_faces/complication/orrery_face.h index 98060d9..a416afd 100644 --- a/movement/watch_faces/complication/orrery_face.h +++ b/movement/watch_faces/complication/orrery_face.h @@ -25,6 +25,48 @@ #ifndef ORRERY_FACE_H_ #define ORRERY_FACE_H_ +/* + * ORRERY face + * + * The Orrery watch face is similar to the Astronomy watch face in that it + * calculates properties of the planets, but instead of calculating their + * positions in the sky, this watch face calculates their absolute locations + * in the solar system. This is only useful if you want to plot the planets + * on graph paper, but hey, you never know! + * + * The controls are identical to the Astronomy watch face: while the title + * screen (“Orrery”) is displayed, you can advance through the available + * planets with repeated short presses on the Alarm button. The available + * planets: + * + * ME - Mercury + * VE - Venus + * EA - Earth + * LU - Luna, the Earth’s moon + * MA - Mars + * JU - Jupiter + * SA - Saturn + * UR - Uranus + * NE - Neptune + * + * Note that the sun is not available in this menu, as the sun is always at + * (0,0,0) in this calculation. + * + * Long press on the Alarm button to calculate the planet’s location, and + * after a flashing “C” (for Calculating), you will be presented with the + * planet’s X coordinate in astronomical units. Short press Alarm to cycle + * through the X, Y and Z coordinates, and then long press Alarm to return + * to planet selection. + * + * The large numbers represent the whole number part, and the two smaller + * numbers (in the seconds place) represent the decimal portion. So if you + * see “SA X 736” and “SA Y -662”, you can read that as an X coordinate of + * 7.36 AU and a Y coordinate of -6.62 AU. You can literally draw a dot at + * (0, 0) to represent the sun, and a dot at (7.36, -6.62) to represent + * Saturn. (The Z coordinates tend to be pretty close to zero, as the + * planets largely orbit on a single plane, the ecliptic.) + */ + #include "movement.h" typedef enum { diff --git a/movement/watch_faces/complication/planetary_hours_face.h b/movement/watch_faces/complication/planetary_hours_face.h index 53237df..dfa6801 100644 --- a/movement/watch_faces/complication/planetary_hours_face.h +++ b/movement/watch_faces/complication/planetary_hours_face.h @@ -26,12 +26,11 @@ #ifndef planetary_hours_face_H_ #define planetary_hours_face_H_ -#include "movement.h" -#include "sunrise_sunset_face.h" - /* - * BACKGROUND - + * PLANETARY HOURS face + * + * Background + * * Both the 24 hour day and the order of our weekdays have quite esoteric roots. * The ancient Egyptians divided the day up into 12 hours of sunlight and 12 hours * of night time. Obviously the length of these hours varied throughout the year. @@ -74,6 +73,9 @@ * watch face to work properly!) */ +#include "movement.h" +#include "sunrise_sunset_face.h" + typedef struct { // Anything you need to keep track of, put it here! uint32_t planetary_hours[24]; diff --git a/movement/watch_faces/complication/planetary_time_face.h b/movement/watch_faces/complication/planetary_time_face.h index 0ecc11a..b7e8e80 100644 --- a/movement/watch_faces/complication/planetary_time_face.h +++ b/movement/watch_faces/complication/planetary_time_face.h @@ -26,12 +26,11 @@ #ifndef planetary_time_face_H_ #define planetary_time_face_H_ -#include "movement.h" -#include "sunrise_sunset_face.h" - /* + * PLANETARY TIME face + * * BACKGROUND - + * * Both the 24 hour day and the order of our weekdays have quite esoteric roots. * The ancient Egyptians divided the day up into 12 hours of sunlight and 12 hours * of night time. Obviously the length of these hours varied throughout the year. @@ -77,6 +76,9 @@ * watch face to work properly!) */ +#include "movement.h" +#include "sunrise_sunset_face.h" + typedef struct { // Anything you need to keep track of, put it here! uint32_t phase_start; diff --git a/movement/watch_faces/complication/probability_face.h b/movement/watch_faces/complication/probability_face.h index c6d3638..b253064 100644 --- a/movement/watch_faces/complication/probability_face.h +++ b/movement/watch_faces/complication/probability_face.h @@ -25,6 +25,18 @@ #ifndef PROBABILITY_FACE_H_ #define PROBABILITY_FACE_H_ +/* + * PROBABILITY face + * + * This face is a dice-rolling random number generator. + * Supports dice with 2, 4, 6, 8, 10, 12, 20, or 100 sides. + * + * Press LIGHT to cycle through die type. + * The current die size is indicated on the left ("C" for 100) + * + * Press ALARM to roll the selected die. + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/complication/pulsometer_face.c b/movement/watch_faces/complication/pulsometer_face.c index 2247421..3c04aa1 100644 --- a/movement/watch_faces/complication/pulsometer_face.c +++ b/movement/watch_faces/complication/pulsometer_face.c @@ -1,7 +1,11 @@ +/* SPDX-License-Identifier: MIT */ + /* * MIT License * - * Copyright (c) 2022 Joey Castillo + * Copyright © 2021-2022 Joey Castillo + * Copyright © 2023 Jeremy O'Brien + * Copyright © 2024 Matheus Afonso Martins Moreira (https://www.matheusmoreira.com/) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,73 +28,162 @@ #include #include + #include "pulsometer_face.h" #include "watch.h" -#define PULSOMETER_FACE_FREQUENCY_FACTOR (4ul) // refresh rate will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.) +#ifndef PULSOMETER_FACE_TITLE +#define PULSOMETER_FACE_TITLE "PL" +#endif + +#ifndef PULSOMETER_FACE_CALIBRATION_DEFAULT +#define PULSOMETER_FACE_CALIBRATION_DEFAULT (30) +#endif + +#ifndef PULSOMETER_FACE_CALIBRATION_INCREMENT +#define PULSOMETER_FACE_CALIBRATION_INCREMENT (10) +#endif + +// tick frequency will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.) +#ifndef PULSOMETER_FACE_FREQUENCY_FACTOR +#define PULSOMETER_FACE_FREQUENCY_FACTOR (4ul) +#endif + #define PULSOMETER_FACE_FREQUENCY (1 << PULSOMETER_FACE_FREQUENCY_FACTOR) +typedef struct { + bool measuring; + int16_t pulses; + int16_t ticks; + int8_t calibration; +} pulsometer_state_t; + +static void pulsometer_display_title(pulsometer_state_t *pulsometer) { + watch_display_string(PULSOMETER_FACE_TITLE, 0); +} + +static void pulsometer_display_calibration(pulsometer_state_t *pulsometer) { + char buf[3]; + snprintf(buf, sizeof(buf), "%2hhd", pulsometer->calibration); + watch_display_string(buf, 2); +} + +static void pulsometer_display_measurement(pulsometer_state_t *pulsometer) { + char buf[7]; + snprintf(buf, sizeof(buf), "%-6hd", pulsometer->pulses); + watch_display_string(buf, 4); +} + +static void pulsometer_indicate(pulsometer_state_t *pulsometer) { + if (pulsometer->measuring) { + watch_set_indicator(WATCH_INDICATOR_LAP); + } else { + watch_clear_indicator(WATCH_INDICATOR_LAP); + } +} + +static void pulsometer_start_measurement(pulsometer_state_t *pulsometer) { + pulsometer->measuring = true; + pulsometer->pulses = INT16_MAX; + pulsometer->ticks = 0; + + pulsometer_indicate(pulsometer); + + movement_request_tick_frequency(PULSOMETER_FACE_FREQUENCY); +} + +static void pulsometer_measure(pulsometer_state_t *pulsometer) { + if (!pulsometer->measuring) { return; } + + pulsometer->ticks++; + + float ticks_per_minute = 60 << PULSOMETER_FACE_FREQUENCY_FACTOR; + float pulses_while_button_held = ticks_per_minute / pulsometer->ticks; + float calibrated_pulses = pulses_while_button_held * pulsometer->calibration; + calibrated_pulses += 0.5f; + + pulsometer->pulses = (int16_t) calibrated_pulses; + + pulsometer_display_measurement(pulsometer); +} + +static void pulsometer_stop_measurement(pulsometer_state_t *pulsometer) { + movement_request_tick_frequency(1); + + pulsometer->measuring = false; + + pulsometer_display_measurement(pulsometer); + pulsometer_indicate(pulsometer); +} + +static void pulsometer_cycle_calibration(pulsometer_state_t *pulsometer, int8_t increment) { + if (pulsometer->measuring) { return; } + + if (pulsometer->calibration <= 0) { + pulsometer->calibration = 1; + } + + int8_t last = pulsometer->calibration; + pulsometer->calibration += increment; + + if (pulsometer->calibration > 39) { + pulsometer->calibration = last == 39? 1 : 39; + } + + pulsometer_display_calibration(pulsometer); +} + void pulsometer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { (void) settings; (void) watch_face_index; - if (*context_ptr == NULL) *context_ptr = malloc(sizeof(pulsometer_state_t)); + + if (*context_ptr == NULL) { + pulsometer_state_t *pulsometer = malloc(sizeof(pulsometer_state_t)); + + pulsometer->calibration = PULSOMETER_FACE_CALIBRATION_DEFAULT; + pulsometer->pulses = 0; + pulsometer->ticks = 0; + + *context_ptr = pulsometer; + } } void pulsometer_face_activate(movement_settings_t *settings, void *context) { (void) settings; - memset(context, 0, sizeof(pulsometer_state_t)); + + pulsometer_state_t *pulsometer = context; + + pulsometer->measuring = false; + + pulsometer_display_title(pulsometer); + pulsometer_display_calibration(pulsometer); + pulsometer_display_measurement(pulsometer); } bool pulsometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { (void) settings; - pulsometer_state_t *pulsometer_state = (pulsometer_state_t *)context; - char buf[14]; + + pulsometer_state_t *pulsometer = (pulsometer_state_t *) context; + switch (event.event_type) { case EVENT_ALARM_BUTTON_DOWN: - pulsometer_state->measuring = true; - pulsometer_state->pulse = 0xFFFF; - pulsometer_state->ticks = 0; - movement_request_tick_frequency(PULSOMETER_FACE_FREQUENCY); + pulsometer_start_measurement(pulsometer); break; case EVENT_ALARM_BUTTON_UP: case EVENT_ALARM_LONG_UP: - pulsometer_state->measuring = false; - movement_request_tick_frequency(1); + pulsometer_stop_measurement(pulsometer); break; case EVENT_TICK: - if (pulsometer_state->pulse == 0 && !pulsometer_state->measuring) { - switch (pulsometer_state->ticks % 5) { - case 0: - watch_display_string(" Hold ", 2); - break; - case 1: - watch_display_string(" Alarn", 4); - break; - case 2: - watch_display_string("* Count ", 0); - break; - case 3: - watch_display_string(" 30Beats ", 0); - break; - case 4: - watch_clear_display(); - break; - } - pulsometer_state->ticks = (pulsometer_state->ticks + 1) % 5; - } else { - if (pulsometer_state->measuring && pulsometer_state->ticks) { - pulsometer_state->pulse = (int16_t)((30.0 * ((float)(60 << PULSOMETER_FACE_FREQUENCY_FACTOR) / (float)pulsometer_state->ticks)) + 0.5); - } - if (pulsometer_state->pulse > 240) { - watch_display_string(" Hi", 0); - } else if (pulsometer_state->pulse < 40) { - watch_display_string(" Lo", 0); - } else { - sprintf(buf, " %-3dbpn", pulsometer_state->pulse); - watch_display_string(buf, 0); - } - if (pulsometer_state->measuring) pulsometer_state->ticks++; - } + pulsometer_measure(pulsometer); + break; + case EVENT_LIGHT_BUTTON_UP: + pulsometer_cycle_calibration(pulsometer, 1); + break; + case EVENT_LIGHT_LONG_UP: + pulsometer_cycle_calibration(pulsometer, PULSOMETER_FACE_CALIBRATION_INCREMENT); + break; + case EVENT_LIGHT_BUTTON_DOWN: + // Inhibit the LED break; case EVENT_TIMEOUT: movement_move_to_face(0); diff --git a/movement/watch_faces/complication/pulsometer_face.h b/movement/watch_faces/complication/pulsometer_face.h index 600201e..5c1dae9 100644 --- a/movement/watch_faces/complication/pulsometer_face.h +++ b/movement/watch_faces/complication/pulsometer_face.h @@ -1,7 +1,12 @@ +/* SPDX-License-Identifier: MIT */ + /* * MIT License * - * Copyright (c) 2022 Joey Castillo + * Copyright © 2021-2022 Joey Castillo + * Copyright © 2022 Alexsander Akers + * Copyright © 2023 Alex Utter + * Copyright © 2024 Matheus Afonso Martins Moreira (https://www.matheusmoreira.com/) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -25,13 +30,46 @@ #ifndef PULSOMETER_FACE_H_ #define PULSOMETER_FACE_H_ -#include "movement.h" +/* + * PULSOMETER face + * + * The pulsometer implements a classic mechanical watch complication. + * A mechanical pulsometer involves a chronograph with a scale that + * allows the user to compute the number of heart beats per minute + * in less time. The scale is calibrated, or graduated, for a fixed + * number of heart beats, most often 30. The user starts the chronograph + * and simultaneously begins counting the heart beats. The movement of + * the chronograph's seconds hand over time automatically performs the + * computations required. When the calibrated number of heart beats + * is reached, the chronograph is stopped and the seconds hand shows + * the heart rate. + * + * The Sensor Watch pulsometer improves this design with user calibration: + * it can be graduated to any value between 1 and 39 pulsations per minute. + * The default is still 30, mirroring the classic pulsometer calibration. + * This feature allows the user to reconfigure the pulsometer to count + * many other types of periodic minutely events, making it more versatile. + * For example, it can be set to 5 respirations per minute to turn it into + * an asthmometer, a nearly identical mechanical watch complication + * that doctors might use to quickly measure respiratory rate. + * + * To use the pulsometer, hold the ALARM button and count the pulses. + * When the calibrated number of pulses is reached, release the button. + * The display will show the number of pulses per minute. + * + * In order to measure heart rate, feel for a pulse using the hand with + * the watch while holding the button down with the other. + * The pulse can be easily felt on the carotid artery of the neck. + * + * In order to measure breathing rate, simply hold the ALARM button + * and count the number of breaths. + * + * To calibrate the pulsometer, press LIGHT + * to cycle to the next integer calibration. + * Long press LIGHT to cycle it by 10. + */ -typedef struct { - bool measuring; - int16_t pulse; - int16_t ticks; -} pulsometer_state_t; +#include "movement.h" void pulsometer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); void pulsometer_face_activate(movement_settings_t *settings, void *context); diff --git a/movement/watch_faces/complication/randonaut_face.c b/movement/watch_faces/complication/randonaut_face.c index bca334f..3bbc147 100644 --- a/movement/watch_faces/complication/randonaut_face.c +++ b/movement/watch_faces/complication/randonaut_face.c @@ -357,7 +357,7 @@ static uint32_t _get_true_entropy(void) { while (!hri_trng_get_INTFLAG_reg(TRNG, TRNG_INTFLAG_DATARDY)); // Wait for TRNG data to be ready - hri_trng_clear_CTRLA_ENABLE_bit(TRNG); + watch_disable_TRNG(); hri_mclk_clear_APBCMASK_TRNG_bit(MCLK); return hri_trng_read_DATA_reg(TRNG); // Read a single 32-bit word from TRNG and return it #endif diff --git a/movement/watch_faces/complication/randonaut_face.h b/movement/watch_faces/complication/randonaut_face.h index fabde79..2f18d45 100644 --- a/movement/watch_faces/complication/randonaut_face.h +++ b/movement/watch_faces/complication/randonaut_face.h @@ -25,11 +25,8 @@ #ifndef RANDONAUT_FACE_H_ #define RANDONAUT_FACE_H_ -#include "movement.h" -#include "place_face.h" - /* - * RANDONAUT FACE + * RANDONAUT face * ============== * * Randonauting is a way to turn the world around you into an adventure and get the user outside @@ -71,6 +68,9 @@ * */ +#include "movement.h" +#include "place_face.h" + typedef struct { uint8_t mode :3; uint8_t location_format :3; diff --git a/movement/watch_faces/complication/ratemeter_face.h b/movement/watch_faces/complication/ratemeter_face.h index a1f1849..4b10c0f 100644 --- a/movement/watch_faces/complication/ratemeter_face.h +++ b/movement/watch_faces/complication/ratemeter_face.h @@ -25,6 +25,16 @@ #ifndef RATEMETER_FACE_H_ #define RATEMETER_FACE_H_ +/* + * RATE METER face + * + * The rate meter shows the rate per minute at which the ALARM button is + * being pressed. This is particularly useful in sports where cadence + * tracking is useful. For instance, rowing coaches often use a dedicated + * rate meter - clicking the rate button each time the crew puts their oars + * in the water to see the rate (strokes per minute) on the rate meter. + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/complication/rpn_calculator_alt_face.c b/movement/watch_faces/complication/rpn_calculator_alt_face.c index e48efe8..4a191b7 100644 --- a/movement/watch_faces/complication/rpn_calculator_alt_face.c +++ b/movement/watch_faces/complication/rpn_calculator_alt_face.c @@ -22,39 +22,6 @@ * SOFTWARE. */ -/* RPN Calculator alternate face. - * - * Operations appear in the 'day' section; ALARM changes between operations when operation is flashing. - * LIGHT executes current operation. - * - * This is the alternate face because it has a non-traditional number entry system which - * I call 'guess a number'. In number entry mode, the watch tries to guess which number you - * want, and you respond with 'smaller' (left - MODE) or larger (right - ALARM). This means - * that when you _are_ entering a number, MODE will no longer move between faces! - * - * Example of entering the number 27 - * - select the NO operation (probably unnecessary, as this is the default), - * and execute it by hitting LIGHT. - * - you are now in number entry mode; you know this because nothing is flashing. - * - Watch displays 10; you hit ALARM to say you want a larger number. - * - Watch displays 100; you hit MODE to say you want a smaller number. - * - Continuing: 50 -> MODE -> 30 -> MODE -> 20 -> ALARM -> 27 - * - Hit LIGHT to add the number to the stack (and now 'NO' is flashing - * again, indicating you're back in operation selection mode). - * - * One other thing to watch out for is how quickly it will switch into scientific notation - * due to the limitations of the display when you have large numbers or non-integer values. - * In this mode, the 'colon' serves at the decimal point, and the numbers in the top right - * are the exponent. - * - * As with the main movement firmware, this has the concept of 'secondary' functions which - * you can jump to by a long hold of ALARM on NO. These are functions to do with stack - * manipulation (pop, swap, dupe, clear, size (le)). If you're _not_ on NO, a long - * hold will take you back to it. - * - * See 'functions' below for names of all operations. - */ - #include #include #include diff --git a/movement/watch_faces/complication/rpn_calculator_alt_face.h b/movement/watch_faces/complication/rpn_calculator_alt_face.h index 2a96467..bb4fd7d 100644 --- a/movement/watch_faces/complication/rpn_calculator_alt_face.h +++ b/movement/watch_faces/complication/rpn_calculator_alt_face.h @@ -25,6 +25,40 @@ #ifndef CALCULATOR_FACE_H_ #define CALCULATOR_FACE_H_ +/* + * RPN Calculator alternate face. + * + * Operations appear in the 'day' section; ALARM changes between operations when + * operation is flashing. LIGHT executes current operation. + * + * This is the alternate face because it has a non-traditional number entry system which + * I call 'guess a number'. In number entry mode, the watch tries to guess which number you + * want, and you respond with 'smaller' (left - MODE) or larger (right - ALARM). This means + * that when you _are_ entering a number, MODE will no longer move between faces! + * + * Example of entering the number 27 + * - select the NO operation (probably unnecessary, as this is the default), + * and execute it by hitting LIGHT. + * - you are now in number entry mode; you know this because nothing is flashing. + * - Watch displays 10; you hit ALARM to say you want a larger number. + * - Watch displays 100; you hit MODE to say you want a smaller number. + * - Continuing: 50 -> MODE -> 30 -> MODE -> 20 -> ALARM -> 27 + * - Hit LIGHT to add the number to the stack (and now 'NO' is flashing + * again, indicating you're back in operation selection mode). + * + * One other thing to watch out for is how quickly it will switch into scientific notation + * due to the limitations of the display when you have large numbers or non-integer values. + * In this mode, the 'colon' serves at the decimal point, and the numbers in the top right + * are the exponent. + * + * As with the main movement firmware, this has the concept of 'secondary' functions which + * you can jump to by a long hold of ALARM on NO. These are functions to do with stack + * manipulation (pop, swap, dupe, clear, size (le)). If you're _not_ on NO, a long + * hold will take you back to it. + * + * See 'functions' in "rpn_calculator_alt_face.c" for names of all operations. + */ + #include "movement.h" #define CALC_MAX_STACK_SIZE 20 diff --git a/movement/watch_faces/complication/rpn_calculator_face.h b/movement/watch_faces/complication/rpn_calculator_face.h index b47eeea..57b59a2 100644 --- a/movement/watch_faces/complication/rpn_calculator_face.h +++ b/movement/watch_faces/complication/rpn_calculator_face.h @@ -25,6 +25,15 @@ #ifndef RPN_CALCULATOR_FACE_H_ #define RPN_CALCULATOR_FACE_H_ +/* + * RPN CALCULATOR face + * + * A calculator face using reverse polish notation (RPN). + * + * For usage instructions, please refer to the wiki: + * https://www.sensorwatch.net/docs/watchfaces/complication/#rpn-calculator + */ + #include "movement.h" #define RPN_CALCULATOR_STACK_SIZE 4 diff --git a/movement/watch_faces/complication/sailing_face.c b/movement/watch_faces/complication/sailing_face.c index 748c4f5..a6c13fe 100644 --- a/movement/watch_faces/complication/sailing_face.c +++ b/movement/watch_faces/complication/sailing_face.c @@ -24,45 +24,12 @@ * SOFTWARE. */ -//----------------------------------------------------------------------------- - #include #include #include "sailing_face.h" #include "watch.h" #include "watch_utility.h" -/* - -Implements a sailing timer. - -Usage: - -Waiting mode: Light button enters settings, alarm button starts the timer (sailing mode). - -Sailing mode: -Alarm button switches to next programmed start signal, long press on light button -resets timer and enters waiting mode. Countdown to zero, then switch to counting mode. - -Counting mode: -After the start signal (0s), the duration of the race is counted (like a stopwatch timer). -Alarm button increases the lap counter, alarm long press resets lap counter. -Long press on light button resets timer and enters waiting mode. - -Setting mode: -Alarm button increases active (blinking) signal. Goes to 0 if upper boundary -(11 or whatever the signal left to the active one is set to) is met. -10 is printed vertically (letter o plus top segment). -Alarm button long press resets to default minutes (5-4-1-0). -Light button cycles through the signals. -Long press on light button cycles through sound modes: -- Bell indicator: Sound at start (0s) only. -- Signal indicator: Sound at each programmed signal and at start. -- Bell+Signal: Sound at each minute, at 30s and at 10s countdown. -- No indicator: No sound. - -*/ - #define sl_SELECTIONS 6 #define DEFAULT_MINUTES { 5,4,1,0,0,0 } diff --git a/movement/watch_faces/complication/sailing_face.h b/movement/watch_faces/complication/sailing_face.h index 0f9fd9d..5546f27 100644 --- a/movement/watch_faces/complication/sailing_face.h +++ b/movement/watch_faces/complication/sailing_face.h @@ -24,17 +24,43 @@ * SOFTWARE. */ -//----------------------------------------------------------------------------- - #ifndef SAILING_FACE_H_ #define SAILING_FACE_H_ -#include "movement.h" - /* -A sailing sailing/timer face -*/ + * SAILING face + * Implements a sailing timer. + * + * Usage: + * + * Waiting mode: + * LIGHT button enters settings + * ALARM button starts the timer (sailing mode). + * + * Sailing mode: + * ALARM button switches to next programmed start signal. + * Long press on LIGHT button resets timer and enters waiting mode. + * Countdown to zero, then switch to counting mode. + * + * Counting mode: + * After the start signal (0s), the duration of the race is counted (like a stopwatch timer). + * ALARM button increases the lap counter, ALARM long press resets lap counter. + * Long press on LIGHT button resets timer and enters waiting mode. + * + * Setting mode: + * ALARM button increases active (blinking) signal. Goes to 0 if upper boundary + * (11 or whatever the signal left to the active one is set to) is met. + * 10 is printed vertically (letter o plus top segment). + * ALARM button long press resets to default minutes (5-4-1-0). + * LIGHT button cycles through the signals. + * Long press on LIGHT button cycles through sound modes: + * - Bell indicator: Sound at start (0s) only. + * - Signal indicator: Sound at each programmed signal and at start. + * - Bell+Signal: Sound at each minute, at 30s and at 10s countdown. + * - No indicator: No sound. + */ +#include "movement.h" typedef enum { sl_waiting, diff --git a/movement/watch_faces/complication/ships_bell_face.h b/movement/watch_faces/complication/ships_bell_face.h index dd37731..3a04b93 100644 --- a/movement/watch_faces/complication/ships_bell_face.h +++ b/movement/watch_faces/complication/ships_bell_face.h @@ -25,9 +25,8 @@ #ifndef SHIPS_BELL_FACE_H_ #define SHIPS_BELL_FACE_H_ -#include "movement.h" - /* + * SHIP'S BELL face * A ship's bell complication. * * See: https://en.wikipedia.org/wiki/Ship%27s_bell#Simpler_system @@ -45,6 +44,8 @@ * - long press Alarm button: Cycle through the watches (All/1/2/3) */ +#include "movement.h" + typedef struct { bool bell_enabled; uint8_t on_watch; 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 0000000..64431f9 --- /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 0000000..f5e223b --- /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_ + diff --git a/movement/watch_faces/complication/solstice_face.c b/movement/watch_faces/complication/solstice_face.c new file mode 100644 index 0000000..e74f878 --- /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 0000000..ec537c0 --- /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_ + diff --git a/movement/watch_faces/complication/stock_stopwatch_face.h b/movement/watch_faces/complication/stock_stopwatch_face.h index d8880df..6796a84 100644 --- a/movement/watch_faces/complication/stock_stopwatch_face.h +++ b/movement/watch_faces/complication/stock_stopwatch_face.h @@ -25,12 +25,34 @@ #ifndef STOCK_STOPWATCH_FACE_H_ #define STOCK_STOPWATCH_FACE_H_ -#include "movement.h" +/* + * STOCK STOPWATCH face + * + * The Stock Stopwatch face implements the original F-91W stopwatch + * functionality, including counting hundredths of seconds and lap timing. + * + * Use the ALARM button to start and stop the stopwatch. + * Press the LIGHT button while the stopwatch is running to view the lap time. + * (The stopwatch continues running in the background, indicated by a blinking colon.) + * Press the LIGHT button again to switch back to the running stopwatch. + * Press the LIGHT button when the timekeeping is stopped to reset the stopwatch. + * + * There are two improvements compared to the original F-91W: + * o When the stopwatch reaches 59:59, the counter does not simply jump back + * to zero but keeps track of hours in the upper right-hand corner + * (up to 24 hours). + * o Long-press the light button to toggle the LED behavior. + * It either turns on with each button press or remains off. + * + * NOTE: + * This watch face relies heavily on static vars in stock_stopwatch.c. + * The disadvantage is that you cannot use more than one instance of this + * watch face on your custom firmware - but then again, who would want that? + * The advantage is that accessing vars is more direct and faster, and we + * can save some precious cpu cycles. :-) + */ -// This watch face relies heavily on static vars in stock_stopwatch.c. -// The disadvantage is that you cannot use more than one instance of this watch face on -// your custom firmware - but then again, who would want that? The advantage is that accessing -// vars is more direct and faster, and we can save some precious cpu cycles :-) +#include "movement.h" typedef struct { bool light_on_button; // determines whether the light button actually triggers the led diff --git a/movement/watch_faces/complication/stopwatch_face.h b/movement/watch_faces/complication/stopwatch_face.h index a30c7fb..7bdf19c 100644 --- a/movement/watch_faces/complication/stopwatch_face.h +++ b/movement/watch_faces/complication/stopwatch_face.h @@ -26,6 +26,17 @@ #ifndef STOPWATCH_FACE_H_ #define STOPWATCH_FACE_H_ +/* + * STOPWATCH FACE + * + * The Stopwatch face provides basic stopwatch functionality: you can start + * and stop the stopwatch with the alarm button. Pressing the light button + * when the timer is stopped resets it. + * + * This face does not count sub-seconds. + * See also: "stock_stopwatch_face.h" + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/complication/sunrise_sunset_face.c b/movement/watch_faces/complication/sunrise_sunset_face.c index 82de9c6..7330c42 100644 --- a/movement/watch_faces/complication/sunrise_sunset_face.c +++ b/movement/watch_faces/complication/sunrise_sunset_face.c @@ -197,6 +197,8 @@ static void _sunrise_sunset_face_update_settings_display(movement_event_t event, char buf[12]; switch (state->page) { + case 0: + return; case 1: sprintf(buf, "LA %c %04d", state->working_latitude.sign ? '-' : '+', abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude))); break; diff --git a/movement/watch_faces/complication/sunrise_sunset_face.h b/movement/watch_faces/complication/sunrise_sunset_face.h index d3f2879..16e65b7 100644 --- a/movement/watch_faces/complication/sunrise_sunset_face.h +++ b/movement/watch_faces/complication/sunrise_sunset_face.h @@ -25,10 +25,18 @@ #ifndef SUNRISE_SUNSET_FACE_H_ #define SUNRISE_SUNSET_FACE_H_ -#include "movement.h" +/* + * SUNRISE & SUNSET FACE + * + * The Sunrise/Sunset face is designed to display the next sunrise or sunset + * for a given location. It also functions as an interface for setting the + * location register, which other watch faces can use for various purposes. + * + * Refer to the wiki for usage instructions: + * https://www.sensorwatch.net/docs/watchfaces/complication/#sunrisesunset + */ -// The Sunrise/Sunset face is designed to display the next sunrise or sunset for a given location. -// TODO: It also functions as an interface for setting the location register, which other watch faces can use for various purposes. +#include "movement.h" typedef struct { uint8_t sign: 1; // 0-1 diff --git a/movement/watch_faces/complication/tachymeter_face.h b/movement/watch_faces/complication/tachymeter_face.h index 1889b40..3b23525 100644 --- a/movement/watch_faces/complication/tachymeter_face.h +++ b/movement/watch_faces/complication/tachymeter_face.h @@ -25,6 +25,49 @@ #ifndef TACHYMETER_FACE_H_ #define TACHYMETER_FACE_H_ +/* + * TACHYMETER face + * + * The Tachymeter complication emulates the tachymeter function often + * present in watches, that computes the average speed in [units per hour] + * for a given distance given in [units]. + * + * Use case: + * User sets the distance + * User starts the tachymeter when the trip begins + * User stops the tachymeter when the trip ends + * The watch presents the average speed and trip duration in seconds + * + * Usage: + * Go to tachymeter face, TC is shown in the Weekday Digits + * A steady d in the Day Digits indicates the distance to be used. + * To edit the distance: + * Long-press the Alarm button, the distance edition page (d will blink) + * Use the Light button to change the editing (blinking) digit, and press Alarm to increase its value + * Once done, long-press the Alarm button to exit the distance edition page + * Press the Alarm button to start the tachymeter. + * A running animation will appear in the Day Digits + * Press the Alarm button to stop the tachymeter + * The average speed and total time information will alternate. + * The average speed will be shown alongside /h in the Day Digits; + * and the total time will be shown alongside t in the Day Digits. + * Long press the Light button to return to the distance d page, + * and restart the tachymeter from there. + * Long-press the light button in the steady distance page to reset + * the distance to 1.00 + * + * Pending design points + * o movement_request_tick_frequency(4) is used to obtain a 4Hz ticking, thus + * having a time resolution of 250 ms. Not sure if using event.subsecond` + * is the proper way to get the fractions of second for the start and + * final times. + * o For distance and average speed, the Second Digits (position 8 and 9) + * can be seen as decimals, thus possible to show distances as short as + * 0.01 km (or miles) and speeds as low as 0.01 km/h (or mph). However, + * if the same idea is used for the total time (showing hundredths), + * this limits the display to 9999.99 seconds (~2h:45m). + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/complication/tally_face.h b/movement/watch_faces/complication/tally_face.h index 6584e87..8096592 100644 --- a/movement/watch_faces/complication/tally_face.h +++ b/movement/watch_faces/complication/tally_face.h @@ -25,11 +25,17 @@ #ifndef TALLY_FACE_H_ #define TALLY_FACE_H_ -#include "movement.h" +/* + * TALLY face + * + * Tally face is designed to act as a tally counter. + * Based on the counter_face watch face by Shogo Okamoto. + * + * To advance the counter, press the ALARM button. + * To reset, long press the ALARM button. + */ -// Tally face is designed to act as a tally counter. -// Based on the counter_face watch face by Shogo Okamoto. -// To advance the counter, press the Alarm button. To reset, long press the Alarm button. +#include "movement.h" typedef struct { uint32_t tally_idx; diff --git a/movement/watch_faces/complication/tarot_face.h b/movement/watch_faces/complication/tarot_face.h index 9dfe8b3..b48a7ae 100644 --- a/movement/watch_faces/complication/tarot_face.h +++ b/movement/watch_faces/complication/tarot_face.h @@ -25,10 +25,8 @@ #ifndef TAROT_FACE_H_ #define TAROT_FACE_H_ -#include "movement.h" - /* - * Tarot card watch face + * TAROT CARD watch face * * Draw from a deck of tarot cards. Can choose between major arcana only or * entire deck. @@ -62,6 +60,8 @@ * - Light button (long press): go back to Draw screen, for choosing different draw parameters. */ +#include "movement.h" + #define MAX_CARDS_TO_DRAW 10 typedef struct { diff --git a/movement/watch_faces/complication/tempchart_face.c b/movement/watch_faces/complication/tempchart_face.c index 53b027d..6c6d1bd 100644 --- a/movement/watch_faces/complication/tempchart_face.c +++ b/movement/watch_faces/complication/tempchart_face.c @@ -20,11 +20,6 @@ * 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. - * - * Gathers temperature statistics in a chart form. Statistics bins are per hour / per 0.5°C. - * Saved to file every day at 00:00. Can help improve watch precision in the future. - * If you can gather statistics over few months, and then send tempchart.ini to 3@14.by - it - * will help future generations of precision quartz watches. */ #include diff --git a/movement/watch_faces/complication/tempchart_face.h b/movement/watch_faces/complication/tempchart_face.h index ce870c8..3c9a389 100644 --- a/movement/watch_faces/complication/tempchart_face.h +++ b/movement/watch_faces/complication/tempchart_face.h @@ -25,6 +25,19 @@ #ifndef TEMPCHART_FACE_H_ #define TEMPCHART_FACE_H_ +/* + * TEMPERATURE CHART face + * + * Gathers temperature statistics in a chart form. + * Statistics bins are per hour / per 0.5°C. + * + * Saved to file every day at 00:00. + * Can help improve watch precision in the future. + * + * If you can gather statistics over few months, and then send "tempchart.ini" + * to "3@14.by", it will help future generations of precision quartz watches. + */ + #include "movement.h" void tempchart_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/movement/watch_faces/complication/time_left_face.h b/movement/watch_faces/complication/time_left_face.h index 0ed7fd2..805fd19 100644 --- a/movement/watch_faces/complication/time_left_face.h +++ b/movement/watch_faces/complication/time_left_face.h @@ -25,9 +25,9 @@ #ifndef TIME_LEFT_FACE_H_ #define TIME_LEFT_FACE_H_ -#include "movement.h" - /* + * TIME LEFT face + * * The Time Left Face helps you to visualize how far you have proceeded in a certain * time span. Much like the Day One Face, you can set your beginning date. In addition * to that, you also set your target or destination date. You can then use the face @@ -65,6 +65,8 @@ * */ +#include "movement.h" + typedef struct { uint8_t current_page; uint16_t current_year; diff --git a/movement/watch_faces/complication/timer_face.c b/movement/watch_faces/complication/timer_face.c index 70f250a..29392d6 100644 --- a/movement/watch_faces/complication/timer_face.c +++ b/movement/watch_faces/complication/timer_face.c @@ -22,15 +22,13 @@ * SOFTWARE. */ -//----------------------------------------------------------------------------- - #include #include #include "timer_face.h" #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 +197,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]; } } diff --git a/movement/watch_faces/complication/timer_face.h b/movement/watch_faces/complication/timer_face.h index 5f035cb..3302f12 100644 --- a/movement/watch_faces/complication/timer_face.h +++ b/movement/watch_faces/complication/timer_face.h @@ -22,14 +22,11 @@ * SOFTWARE. */ -//----------------------------------------------------------------------------- - #ifndef TIMER_FACE_H_ #define TIMER_FACE_H_ -#include "movement.h" - /* + * TIMER face * Advanced timer/countdown face with pre-set timer lengths * * This watch face provides the functionality of starting a countdown by choosing @@ -53,6 +50,8 @@ * */ +#include "movement.h" + #define TIMER_SLOTS 9 // offer 9 timer slots typedef enum { diff --git a/movement/watch_faces/complication/tomato_face.c b/movement/watch_faces/complication/tomato_face.c index 698301e..3d46ba9 100644 --- a/movement/watch_faces/complication/tomato_face.c +++ b/movement/watch_faces/complication/tomato_face.c @@ -84,8 +84,10 @@ static void tomato_draw(tomato_state_t *state) { sec = 0; break; } - sprintf(buf, "TO %c%2d%02d%2d", kind, min, sec, state->done_count); - watch_display_string(buf, 0); + if (state->visible) { + sprintf(buf, "TO %c%2d%02d%2d", kind, min, sec, state->done_count); + watch_display_string(buf, 0); + } } static void tomato_reset(tomato_state_t *state) { @@ -116,6 +118,7 @@ void tomato_face_setup(movement_settings_t *settings, uint8_t watch_face_index, state->mode=tomato_ready; state->kind= tomato_focus; state->done_count = 0; + state->visible = true; } } @@ -127,6 +130,7 @@ void tomato_face_activate(movement_settings_t *settings, void *context) { watch_set_indicator(WATCH_INDICATOR_BELL); } watch_set_colon(); + state->visible = true; } bool tomato_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { @@ -184,6 +188,8 @@ bool tomato_face_loop(movement_event_t event, movement_settings_t *settings, voi } void tomato_face_resign(movement_settings_t *settings, void *context) { + tomato_state_t *state = (tomato_state_t *)context; + state->visible = false; (void) settings; (void) context; } diff --git a/movement/watch_faces/complication/tomato_face.h b/movement/watch_faces/complication/tomato_face.h index 5404ad1..25f7db0 100644 --- a/movement/watch_faces/complication/tomato_face.h +++ b/movement/watch_faces/complication/tomato_face.h @@ -25,6 +25,26 @@ #ifndef TOMATO_FACE_H_ #define TOMATO_FACE_H_ +/* + * TOMATO TIMER face + * + * Add a "tomato" timer watch face that alternates between 25 and 5 minute + * timers as in the Pomodoro Technique. + * https://en.wikipedia.org/wiki/Pomodoro_Technique + * + * The top right letter shows mode (f for focus or b for break). + * The bottom right shows how many focus sessions you've completed. + * (You can reset the count with a long press of alarm) + * + * When you show up and it says 25 minutes, you can start it (alarm), + * switch to 5 minute (light) mode or leave (mode). + * + * When it's running you can reset (alarm), or leave (mode). + * + * When it's done, we beep and go back to step 1, changing switching + * mode from focus to break (or break to focus) + */ + #include "movement.h" typedef enum { @@ -44,6 +64,7 @@ typedef struct { tomato_mode mode; tomato_kind kind; uint8_t done_count; + bool visible; } tomato_state_t; void tomato_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/movement/watch_faces/complication/toss_up_face.c b/movement/watch_faces/complication/toss_up_face.c index 08dd005..cf6ca68 100644 --- a/movement/watch_faces/complication/toss_up_face.c +++ b/movement/watch_faces/complication/toss_up_face.c @@ -255,7 +255,8 @@ uint32_t get_true_entropy(void) { while (!hri_trng_get_INTFLAG_reg(TRNG, TRNG_INTFLAG_DATARDY)); // Wait for TRNG data to be ready - hri_trng_clear_CTRLA_ENABLE_bit(TRNG); + watch_disable_TRNG(); + hri_mclk_clear_APBCMASK_TRNG_bit(MCLK); return hri_trng_read_DATA_reg(TRNG); // Read a single 32-bit word from TRNG and return it #endif diff --git a/movement/watch_faces/complication/toss_up_face.h b/movement/watch_faces/complication/toss_up_face.h index ca6136a..cff3409 100644 --- a/movement/watch_faces/complication/toss_up_face.h +++ b/movement/watch_faces/complication/toss_up_face.h @@ -25,10 +25,8 @@ #ifndef TOSS_UP_FACE_H_ #define TOSS_UP_FACE_H_ -#include "movement.h" - /* - * TOSS UP FACE + * TOSS UP face * ============ * * Playful watch face for games of chance or divination using coins or dice. @@ -75,6 +73,8 @@ * */ +#include "movement.h" + typedef struct { // Anything you need to keep track of, put it here! uint32_t entropy; diff --git a/movement/watch_faces/complication/totp_face.c b/movement/watch_faces/complication/totp_face.c index a655018..a593e9c 100644 --- a/movement/watch_faces/complication/totp_face.c +++ b/movement/watch_faces/complication/totp_face.c @@ -1,104 +1,237 @@ +/* SPDX-License-Identifier: MIT */ + +/* + * MIT License + * + * Copyright © 2021 Wesley Ellis (https://github.com/tahnok) + * Copyright © 2021-2023 Joey Castillo + * Copyright © 2022 Jack Bond-Preston + * Copyright © 2023 Alex Utter + * Copyright © 2023 Emilien Court + * Copyright © 2023 Jeremy O'Brien + * Copyright © 2024 Matheus Afonso Martins Moreira (https://www.matheusmoreira.com/) + * Copyright © 2024 Max Zettlmeißl + * + * 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 "totp_face.h" #include "watch.h" #include "watch_utility.h" #include "TOTP.h" +#include "base32.h" -// Use https://cryptii.com/pipes/base32-to-hex to convert base32 to hex -// Use https://github.com/susam/mintotp to generate test codes for verification -// Available algorothms: -// SHA1 (most TOTP codes use this) -// SHA224 -// SHA256 -// SHA384 -// SHA512 +#ifndef TOTP_FACE_MAX_KEY_LENGTH +#define TOTP_FACE_MAX_KEY_LENGTH 128 +#endif + +typedef struct { + unsigned char labels[2]; + hmac_alg algorithm; + uint32_t period; + size_t encoded_key_length; + unsigned char *encoded_key; +} totp_t; + +#define CREDENTIAL(label, key_array, algo, timestep) \ + (const totp_t) { \ + .encoded_key = ((unsigned char *) key_array), \ + .encoded_key_length = sizeof(key_array) - 1, \ + .period = (timestep), \ + .labels = (#label), \ + .algorithm = (algo), \ + } //////////////////////////////////////////////////////////////////////////////// // Enter your TOTP key data below -static const uint8_t num_keys = 2; -static uint8_t keys[] = { - 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0xde, 0xad, 0xbe, 0xef, // 1 - JBSWY3DPEHPK3PXP - 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0xde, 0xad, 0xbe, 0xef, // 2 - JBSWY3DPEHPK3PXP -}; -static const uint8_t key_sizes[] = { - 10, - 10, -}; -static const uint32_t timesteps[] = { - 30, - 30, -}; -static const char labels[][2] = { - { '2', 'F' }, - { 'A', 'C' }, -}; -static const hmac_alg algorithms[] = { - SHA1, - SHA1, + +static totp_t credentials[] = { + CREDENTIAL(2F, "JBSWY3DPEHPK3PXP", SHA1, 30), + CREDENTIAL(AC, "JBSWY3DPEHPK3PXP", SHA1, 30), }; + // END OF KEY DATA. //////////////////////////////////////////////////////////////////////////////// -static void _update_display(totp_state_t *totp_state) { +static inline totp_t *totp_at(size_t i) { + return &credentials[i]; +} + +static inline totp_t *totp_current(totp_state_t *totp_state) { + return totp_at(totp_state->current_index); +} + +static inline size_t totp_total(void) { + return sizeof(credentials) / sizeof(*credentials); +} + +static void totp_validate_key_lengths(void) { + for (size_t n = totp_total(), i = 0; i < n; ++i) { + totp_t *totp = totp_at(i); + + if (UNBASE32_LEN(totp->encoded_key_length) > TOTP_FACE_MAX_KEY_LENGTH) { + // Key exceeds static limits, turn it off by zeroing the length + totp->encoded_key_length = 0; + } + } +} + +static void totp_generate(totp_state_t *totp_state) { + totp_t *totp = totp_current(totp_state); + + if (totp->encoded_key_length <= 0) { + // Key exceeded static limits and was turned off + totp_state->current_decoded_key_length = 0; + return; + } + + totp_state->current_decoded_key_length = base32_decode(totp->encoded_key, totp_state->current_decoded_key); + + if (totp_state->current_decoded_key_length == 0) { + // Decoding failed for some reason + // Not a base 32 string? + return; + } + + TOTP( + totp_state->current_decoded_key, + totp_state->current_decoded_key_length, + totp->period, + totp->algorithm + ); +} + +static void totp_display_error(totp_state_t *totp_state) { + char buf[10 + 1]; + totp_t *totp = totp_current(totp_state); + + snprintf(buf, sizeof(buf), "%c%c ERROR ", totp->labels[0], totp->labels[1]); + watch_display_string(buf, 0); +} + +static void totp_display_code(totp_state_t *totp_state) { char buf[14]; div_t result; uint8_t valid_for; + totp_t *totp = totp_current(totp_state); - result = div(totp_state->timestamp, timesteps[totp_state->current_index]); + result = div(totp_state->timestamp, totp->period); if (result.quot != totp_state->steps) { totp_state->current_code = getCodeFromTimestamp(totp_state->timestamp); totp_state->steps = result.quot; } - valid_for = timesteps[totp_state->current_index] - result.rem; - sprintf(buf, "%c%c%2d%06lu", labels[totp_state->current_index][0], labels[totp_state->current_index][1], valid_for, totp_state->current_code); + valid_for = totp->period - result.rem; + sprintf(buf, "%c%c%2d%06lu", totp->labels[0], totp->labels[1], valid_for, totp_state->current_code); watch_display_string(buf, 0); } +static void totp_display(totp_state_t *totp_state) { + if (totp_state->current_decoded_key_length > 0) { + totp_display_code(totp_state); + } else { + totp_display_error(totp_state); + } +} + +static void totp_generate_and_display(totp_state_t *totp_state) { + totp_generate(totp_state); + totp_display(totp_state); +} + +static inline uint32_t totp_compute_base_timestamp(movement_settings_t *settings) { + return watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), movement_timezone_offsets[settings->bit.time_zone] * 60); +} + void totp_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(totp_state_t)); + + totp_validate_key_lengths(); + + if (*context_ptr == NULL) { + totp_state_t *totp = malloc(sizeof(totp_state_t)); + totp->current_decoded_key = malloc(TOTP_FACE_MAX_KEY_LENGTH); + *context_ptr = totp; + } } void totp_face_activate(movement_settings_t *settings, void *context) { (void) settings; - memset(context, 0, sizeof(totp_state_t)); - totp_state_t *totp_state = (totp_state_t *)context; - TOTP(keys, key_sizes[0], timesteps[0], algorithms[0]); - totp_state->timestamp = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), movement_timezone_offsets[settings->bit.time_zone] * 60); - totp_state->current_code = getCodeFromTimestamp(totp_state->timestamp); + + totp_state_t *totp = (totp_state_t *) context; + + totp->timestamp = totp_compute_base_timestamp(settings); + totp->steps = 0; + totp->current_code = 0; + totp->current_index = 0; + totp->current_decoded_key_length = 0; + // totp->current_decoded_key is already initialized in setup + + totp_generate_and_display(totp); } bool totp_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { (void) settings; - totp_state_t *totp_state = (totp_state_t *)context; + + totp_state_t *totp_state = (totp_state_t *) context; switch (event.event_type) { case EVENT_TICK: totp_state->timestamp++; // fall through case EVENT_ACTIVATE: - _update_display(totp_state); + totp_display(totp_state); break; case EVENT_TIMEOUT: movement_move_to_face(0); break; case EVENT_ALARM_BUTTON_UP: - if (totp_state->current_index + 1 < num_keys) { - totp_state->current_key_offset += key_sizes[totp_state->current_index]; + if (totp_state->current_index + 1 < totp_total()) { totp_state->current_index++; } else { // wrap around to first key - totp_state->current_key_offset = 0; totp_state->current_index = 0; } - TOTP(keys + totp_state->current_key_offset, key_sizes[totp_state->current_index], timesteps[totp_state->current_index], algorithms[totp_state->current_index]); - _update_display(totp_state); + + totp_generate_and_display(totp_state); + + break; + case EVENT_LIGHT_BUTTON_UP: + if (totp_state->current_index == 0) { + // Wrap around to the last credential. + totp_state->current_index = totp_total() - 1; + } else { + totp_state->current_index--; + } + + totp_generate_and_display(totp_state); + break; case EVENT_ALARM_BUTTON_DOWN: case EVENT_ALARM_LONG_PRESS: + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_LIGHT_LONG_PRESS: + movement_illuminate_led(); break; default: movement_default_loop_handler(event, settings); diff --git a/movement/watch_faces/complication/totp_face.h b/movement/watch_faces/complication/totp_face.h index cff5191..19a2cd4 100644 --- a/movement/watch_faces/complication/totp_face.h +++ b/movement/watch_faces/complication/totp_face.h @@ -1,6 +1,68 @@ +/* SPDX-License-Identifier: MIT */ + +/* + * MIT License + * + * Copyright © 2021 Wesley Ellis (https://github.com/tahnok) + * Copyright © 2021-2022 Joey Castillo + * Copyright © 2022 Alexsander Akers + * Copyright © 2022 Jack Bond-Preston + * Copyright © 2023 Alex Utter + * Copyright © 2024 Matheus Afonso Martins Moreira (https://www.matheusmoreira.com/) + * Copyright © 2024 Max Zettlmeißl + * + * 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 TOTP_FACE_H_ #define TOTP_FACE_H_ +/* + * TOTP face + * Time-based one-time password (TOTP) generator + * + * Generate one-time passwords often used for two-factor authentication. + * The secret key must be set by hand, by editing "totp_face.c". + * + * Available algorithms: + * o SHA1 (most TOTP codes use this) + * o SHA224 + * o SHA256 + * o SHA384 + * o SHA512 + * + * Instructions: + * o Find your secret key(s) and convert them to the required format. + * o Use https://cryptii.com/pipes/base32-to-hex to convert base32 to hex + * o Use https://github.com/susam/mintotp to generate test codes for verification + * o Edit global variables in "totp_face.c" to configure your stored keys: + * o "keys", "key_sizes", "timesteps", and "algorithms" set the + * cryptographic parameters for each secret key. + * o "labels" sets the two-letter label for each key + * (This replaces the day-of-week indicator) + * o Once finished, remove the two provided examples. + * + * If you have more than one secret key, press ALARM to cycle through them. + * Press LIGHT to cycle in the other direction or keep it pressed longer to + * activate the light. + */ + #include "movement.h" typedef struct { @@ -8,7 +70,8 @@ typedef struct { uint8_t steps; uint32_t current_code; uint8_t current_index; - uint8_t current_key_offset; + uint8_t *current_decoded_key; + size_t current_decoded_key_length; } totp_state_t; void totp_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/movement/watch_faces/complication/totp_face_lfs.c b/movement/watch_faces/complication/totp_face_lfs.c index d52d862..820ad52 100644 --- a/movement/watch_faces/complication/totp_face_lfs.c +++ b/movement/watch_faces/complication/totp_face_lfs.c @@ -1,3 +1,27 @@ +/* + * MIT License + * + * Copyright (c) 2022 Wesley Ellis (https://github.com/tahnok) + * + * 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 @@ -11,24 +35,6 @@ #include "totp_face_lfs.h" -/* Reads from a file totp_uris.txt where each line is what's in a QR code: - * e.g. - * otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example - * otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 - * This is also the same as what Aegis exports in plain-text format. - * - * Minimal sanitisation of input, however. - * - * At the moment, to get the records onto the filesystem, start a serial connection and do: - * echo otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example > totp_uris.txt - * echo otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 >> totp_uris.txt - * (note the double >> in the second one) - * - * You may want to customise the characters that appear to identify the 2FA code. These are just the first two characters of the issuer, - * and it's fine to modify the URI. - */ - - #define MAX_TOTP_RECORDS 20 #define MAX_TOTP_SECRET_SIZE 48 #define TOTP_FILE "totp_uris.txt" @@ -157,7 +163,7 @@ static void totp_face_lfs_read_file(char *filename) { continue; } - // If we found a probably valid TOTP record, keep it. + // If we found a probably valid TOTP record, keep it. if (totp_records[num_totp_records].secret_size) { num_totp_records += 1; } else { @@ -249,8 +255,21 @@ bool totp_face_lfs_loop(movement_event_t event, movement_settings_t *settings, v totp_face_set_record(totp_state, (totp_state->current_index + 1) % num_totp_records); totp_face_display(totp_state); break; + case EVENT_LIGHT_BUTTON_UP: + if (totp_state->current_index - 1 >= 0) { + totp_face_set_record(totp_state, totp_state->current_index - 1); + } else { + // Wrap around to the last record. + totp_face_set_record(totp_state, num_totp_records - 1); + } + totp_face_display(totp_state); + break; case EVENT_ALARM_BUTTON_DOWN: case EVENT_ALARM_LONG_PRESS: + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_LIGHT_LONG_PRESS: + movement_illuminate_led(); break; default: movement_default_loop_handler(event, settings); diff --git a/movement/watch_faces/complication/totp_face_lfs.h b/movement/watch_faces/complication/totp_face_lfs.h index 0f388bc..72ae246 100644 --- a/movement/watch_faces/complication/totp_face_lfs.h +++ b/movement/watch_faces/complication/totp_face_lfs.h @@ -1,6 +1,56 @@ +/* + * MIT License + * + * Copyright (c) 2022 Wesley Ellis (https://github.com/tahnok) + * + * 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 TOTP_FACE_LFS_H_ #define TOTP_FACE_LFS_H_ +/* + * TOTP-LFS face + * Time-based one-time password (TOTP) generator using LFS + * + * Reads from a file "totp_uris.txt", containing a single secret key in a + * series of URLs. Each line is what's in a QR code, e.g.: + * otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example + * otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 + * + * This is also the same as what Aegis exports in plain-text format. + * This face performs minimal sanitisation of input, however. + * + * At the moment, to get the records onto the filesystem, start a serial connection and do: + * echo otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example > totp_uris.txt + * echo otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 >> totp_uris.txt + * (note the double >> in the second one) + * + * You may want to customise the characters that appear to identify the 2FA + * code. These are just the first two characters of the issuer, and it's fine + * to modify the URI. + * + * If you have more than one secret key, press ALARM to cycle through them. + * Press LIGHT to cycle in the other direction or keep it pressed longer to + * activate the light. + */ + #include "movement.h" typedef struct { 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 0000000..a139427 --- /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 0000000..d6e3495 --- /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_ + diff --git a/movement/watch_faces/complication/wake_face.c b/movement/watch_faces/complication/wake_face.c index 5c5f86b..6fa801f 100644 --- a/movement/watch_faces/complication/wake_face.c +++ b/movement/watch_faces/complication/wake_face.c @@ -22,24 +22,12 @@ * SOFTWARE. */ -//----------------------------------------------------------------------------- - #include #include -// #include - #include "wake_face.h" #include "watch.h" #include "watch_utility.h" -/* - UI Notes - º Light advances hour by 1 - º Light long press advances hour by 6 - º Alarm advances minute by 10 - º Alarm long press cycles through signal modes (just one at the moment) -*/ - // // Private // diff --git a/movement/watch_faces/complication/wake_face.h b/movement/watch_faces/complication/wake_face.h index c091c8f..b4a25a9 100644 --- a/movement/watch_faces/complication/wake_face.h +++ b/movement/watch_faces/complication/wake_face.h @@ -22,11 +22,24 @@ * SOFTWARE. */ -//----------------------------------------------------------------------------- - #ifndef WAKE_FACE_H_ #define WAKE_FACE_H_ +/* + * WAKE daily alarm face + * + * Basic daily alarm clock face. Seems useful if nothing else in the interest + * of feature parity with the F-91W’s OEM module, 593. + * + * Also experiments with caret-free UI: One button cycles hours, the other + * minutes, so there’s no toggling between display and adjust modes and no + * cycling the caret through the UI. + * º LIGHT advances hour by 1 + * º LIGHT long press advances hour by 6 + * º ALARM advances minute by 10 + * º ALARM long press cycles through signal modes (just one at the moment) + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/demo/character_set_face.h b/movement/watch_faces/demo/character_set_face.h index 82627ae..2fac26a 100644 --- a/movement/watch_faces/demo/character_set_face.h +++ b/movement/watch_faces/demo/character_set_face.h @@ -25,6 +25,17 @@ #ifndef CHARACTER_SET_FACE_H_ #define CHARACTER_SET_FACE_H_ +/* + * CHARACTER SET FACE + * + * This watch face displays all of the characters in the Sensor Watch character + * set. You can advance from one character to the next with a short press of the + * ALARM button. + * + * This watch face may be useful to watch face developers, in that it can help + * them to understand which characters will work in different positions. + */ + #include "movement.h" void character_set_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/movement/watch_faces/demo/chirpy_demo_face.h b/movement/watch_faces/demo/chirpy_demo_face.h index 2d34107..90a6133 100644 --- a/movement/watch_faces/demo/chirpy_demo_face.h +++ b/movement/watch_faces/demo/chirpy_demo_face.h @@ -25,8 +25,6 @@ #ifndef CHIRPY_DEMO_FACE_H_ #define CHIRPY_DEMO_FACE_H_ -#include "movement.h" - /* * CHIRPY DEMO FACE * @@ -50,9 +48,10 @@ * * To record and decode a chirpy transmission on your computer, you can use the web app here: * https://jealousmarkup.xyz/off/chirpy/rx/ - * */ +#include "movement.h" + void chirpy_demo_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); void chirpy_demo_face_activate(movement_settings_t *settings, void *context); bool chirpy_demo_face_loop(movement_event_t event, movement_settings_t *settings, void *context); diff --git a/movement/watch_faces/demo/demo_face.h b/movement/watch_faces/demo/demo_face.h index 026e0d1..669f387 100644 --- a/movement/watch_faces/demo/demo_face.h +++ b/movement/watch_faces/demo/demo_face.h @@ -25,6 +25,17 @@ #ifndef DEMO_FACE_H_ #define DEMO_FACE_H_ +/* + * DEMO FACE + * + * This watch was designed for the Crowd Supply marketing team, so they could + * photograph the various functions of Sensor Watch. The Alarm button advances + * through static screens that simulate different watch faces. + * + * This watch face may only be useful to you if you need to photograph Sensor + * Watch, i.e. for a blog post. + */ + #include "movement.h" void demo_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/movement/watch_faces/demo/frequency_correction_face.h b/movement/watch_faces/demo/frequency_correction_face.h index 52c4e62..0b9c435 100644 --- a/movement/watch_faces/demo/frequency_correction_face.h +++ b/movement/watch_faces/demo/frequency_correction_face.h @@ -25,6 +25,18 @@ #ifndef FREQUENCY_CORRECTION_FACE_H_ #define FREQUENCY_CORRECTION_FACE_H_ +/* + * FREQUENCY CORRECTION FACE + * + * While active, this face generates a square-wave on pin A1 of the 9-pin + * connector. The output frequency is adjustable from 64 Hz to 0.5 Hz. + * Long-press ALARM to cycle through available frequencies. + * + * This face also displays the value of the watch's frequency-correction + * register. This setting varies from -127 to +127. Press LIGHT to increment + * or ALARM to decrement the setting. + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/demo/hello_there_face.h b/movement/watch_faces/demo/hello_there_face.h index 1140bb2..dc76f5f 100644 --- a/movement/watch_faces/demo/hello_there_face.h +++ b/movement/watch_faces/demo/hello_there_face.h @@ -25,6 +25,13 @@ #ifndef HELLO_THERE_FACE_H_ #define HELLO_THERE_FACE_H_ +/* + * HELLO THERE FACE + * + * A simple demo that displays the word "Hello" and then the word "there", + * on an endless loop. Press ALARM to pause or resume the animation. + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/demo/lis2dw_logging_face.h b/movement/watch_faces/demo/lis2dw_logging_face.h index f6ea85e..ad32518 100644 --- a/movement/watch_faces/demo/lis2dw_logging_face.h +++ b/movement/watch_faces/demo/lis2dw_logging_face.h @@ -25,6 +25,14 @@ #ifndef LIS2DW_LOGGING_FACE_H_ #define LIS2DW_LOGGING_FACE_H_ +/* + * LIS2DW Accelerometer Data Logger + * + * This is an experimental watch face for logging data on the “Sensor Watch + * Motion Express” board. I will add more documentation for this watch face + * once this sensor board is more widely available. + */ + #include "movement.h" #include "watch.h" diff --git a/movement/watch_faces/demo/voltage_face.h b/movement/watch_faces/demo/voltage_face.h index dc5e631..59d0f9e 100644 --- a/movement/watch_faces/demo/voltage_face.h +++ b/movement/watch_faces/demo/voltage_face.h @@ -25,6 +25,17 @@ #ifndef VOLTAGE_FACE_H_ #define VOLTAGE_FACE_H_ +/* + * VOLTAGE face + * + * This watch face is very simple and has no controls to speak of. It displays + * the battery voltage as measured by the SAM L22’s ADC. + * + * Note that the Simple Clock watch face includes a low battery warning, so you + * don’t technically need to this watch face unless you want to track the + * battery level. + */ + #include "movement.h" void voltage_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/movement/watch_faces/sensor/accelerometer_data_acquisition_face.h b/movement/watch_faces/sensor/accelerometer_data_acquisition_face.h index 9cea809..946639d 100644 --- a/movement/watch_faces/sensor/accelerometer_data_acquisition_face.h +++ b/movement/watch_faces/sensor/accelerometer_data_acquisition_face.h @@ -25,6 +25,12 @@ #ifndef ACCELEROMETER_DATA_ACQUISITION_FACE_H_ #define ACCELEROMETER_DATA_ACQUISITION_FACE_H_ +/* + * ACCELEROMETER DATA ACQUISITION + * + * TODO: Add description here, including controls. + */ + #include "movement.h" #define ACCELEROMETER_DATA_ACQUISITION_INVALID ((uint64_t)(0b11)) // all bits are 1 when the flash is erased diff --git a/movement/watch_faces/sensor/lightmeter_face.c b/movement/watch_faces/sensor/lightmeter_face.c index 861e28d..9fb489a 100644 --- a/movement/watch_faces/sensor/lightmeter_face.c +++ b/movement/watch_faces/sensor/lightmeter_face.c @@ -22,37 +22,6 @@ * SOFTWARE. */ -/* Aperture-priority Light Meter Face - * - * Tested with the "Q3Q-SWAB-A1-00 Temperature + Test Points + OPT3001" flexboard. - * This flexboard could use a revision: - * - * - The thermistor components should be moved west a mm or flipped to the backside - * to avoid stressing the flexboard against the processor so much. - * - The 'no connect' pad falls off easily. - * - * Controls: - * - * - Trigger a measurement by long-pressing Alarm. - * Sensor integration is happening when the Signal indicator is on. - * - * - ISO setting can be cycled by long-pressing Light. - * During integration the current ISO setting will be displayed. - * - * - EV measurement in the top right: "LAP" indicates "half stop". - * So "LAP -1" means EV = -1.5. Likewise "LAP 13" means EV = +13.5 - * - * - Aperture in the bottom right: the last 3 main digits are the f-stop. - * Adjust this number in half-stop increments using Alarm = +1/2 and Light = -1/2. - * - * - Best shutter speed in the bottom left: the first 3 digits are the shutter speed. - * Some special chars are needed here: "-" = seconds, "h" = extra half second, "K" = thousands. - * "HI" or "LO" if there's no shutter in the dictionary within 0.5 stops of correct exposure. - * - * - Mode long-press changes the main digits to show raw sensor lux measurements. - * - */ - #include #include #include diff --git a/movement/watch_faces/sensor/lightmeter_face.h b/movement/watch_faces/sensor/lightmeter_face.h index 2f8813f..affae2e 100644 --- a/movement/watch_faces/sensor/lightmeter_face.h +++ b/movement/watch_faces/sensor/lightmeter_face.h @@ -25,6 +25,37 @@ #ifndef LIGHTMETER_FACE_H_ #define LIGHTMETER_FACE_H_ +/* + * Aperture-priority Light Meter Face + * + * Tested with the "Q3Q-SWAB-A1-00 Temperature + Test Points + OPT3001" flexboard. + * This flexboard could use a revision: + * + * - The thermistor components should be moved west a mm or flipped to the backside + * to avoid stressing the flexboard against the processor so much. + * - The 'no connect' pad falls off easily. + * + * Controls: + * + * - Trigger a measurement by long-pressing Alarm. + * Sensor integration is happening when the Signal indicator is on. + * + * - ISO setting can be cycled by long-pressing Light. + * During integration the current ISO setting will be displayed. + * + * - EV measurement in the top right: "LAP" indicates "half stop". + * So "LAP -1" means EV = -1.5. Likewise "LAP 13" means EV = +13.5 + * + * - Aperture in the bottom right: the last 3 main digits are the f-stop. + * Adjust this number in half-stop increments using Alarm = +1/2 and Light = -1/2. + * + * - Best shutter speed in the bottom left: the first 3 digits are the shutter speed. + * Some special chars are needed here: "-" = seconds, "h" = extra half second, "K" = thousands. + * "HI" or "LO" if there's no shutter in the dictionary within 0.5 stops of correct exposure. + * + * - Mode long-press changes the main digits to show raw sensor lux measurements. + */ + #include "movement.h" #include "opt3001.h" diff --git a/movement/watch_faces/sensor/thermistor_logging_face.h b/movement/watch_faces/sensor/thermistor_logging_face.h index 4ba593e..0debfbf 100644 --- a/movement/watch_faces/sensor/thermistor_logging_face.h +++ b/movement/watch_faces/sensor/thermistor_logging_face.h @@ -25,6 +25,34 @@ #ifndef THERMISTOR_LOGGING_FACE_H_ #define THERMISTOR_LOGGING_FACE_H_ +/* + * THERMISTOR LOGGING (aka Temperature Log) + * + * This watch face automatically logs the temperature once an hour, and + * maintains a 36-hour log of readings. This watch face is admittedly rather + * complex, and bears some explanation. + * + * The main display shows the letters “TL” in the top left, indicating the + * name of the watch face. At the top right, it displays the index of the + * reading; 0 represents the most recent reading taken, 1 represents one + * hour earlier, etc. The bottom line in this mode displays the logged + * temperature. + * + * A short press of the “Alarm” button advances to the next oldest reading; + * you will see the number at the top right advance from 0 to 1 to 2, all + * the way to 35, the oldest reading available. + * + * A short press of the “Light” button will briefly display the timestamp + * of the reading. The letters at the top left will display the word “At”, + * and the main line will display the timestamp of the currently displayed + * data point. The number in the top right will display the day of the month + * for the given data point; for example, you can read “At 22 3:00 PM” as + * ”At 3:00 PM on the 22nd”. + * + * If you need to illuminate the LED to read the data point, long press the + * Light button and release it. + */ + #include "movement.h" #include "watch.h" diff --git a/movement/watch_faces/sensor/thermistor_readout_face.h b/movement/watch_faces/sensor/thermistor_readout_face.h index 7361164..10cdcc1 100644 --- a/movement/watch_faces/sensor/thermistor_readout_face.h +++ b/movement/watch_faces/sensor/thermistor_readout_face.h @@ -25,6 +25,29 @@ #ifndef THERMISTOR_READOUT_FACE_H_ #define THERMISTOR_READOUT_FACE_H_ +/* + * THERMISTOR READOUT (aka Temperature Display) + * + * This watch face is designed to work with either the Temperature + GPIO + * sensor board or the Temperature + Light sensor board. It reads the current + * temperature from the thermistor voltage divider on the sensor board, and + * displays the current temperature in degrees Celsius. + * + * When the watch is on your wrist, your body heat interferes with an ambient + * temperature reading, but if you set it on a bedside table, strap it to your + * bike handlebars or place it outside of your tent while camping, this watch + * face can act as a digital thermometer for displaying ambient conditions. + * + * The temperature sensor watch face automatically samples the temperature + * once every five seconds, and it illuminates the Signal indicator just + * before taking a reading. + * + * Pressing the ALARM button toggles the unit display from Celsius to + * Fahrenheit. Technically this sets the global “Metric / Imperial” flag, so + * any other watch face that displays localizable units will display them in + * the system selected here. + */ + #include "movement.h" void thermistor_readout_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/movement/watch_faces/sensor/thermistor_testing_face.c b/movement/watch_faces/sensor/thermistor_testing_face.c index 2910fbd..10addc0 100644 --- a/movement/watch_faces/sensor/thermistor_testing_face.c +++ b/movement/watch_faces/sensor/thermistor_testing_face.c @@ -28,11 +28,6 @@ #include "thermistor_driver.h" #include "watch.h" -// This watch face is designed for testing temperature sensor boards. -// It displays temperature readings at a relatively fast rate of 8 Hz, -// and disables low energy mode so my testing device doesn't sleep. -// You more than likely want to use thermistor_readout_face instead. - static void _thermistor_testing_face_update_display(bool in_fahrenheit) { thermistor_driver_enable(); float temperature_c = thermistor_driver_get_temperature(); diff --git a/movement/watch_faces/sensor/thermistor_testing_face.h b/movement/watch_faces/sensor/thermistor_testing_face.h index 656f58d..9721484 100644 --- a/movement/watch_faces/sensor/thermistor_testing_face.h +++ b/movement/watch_faces/sensor/thermistor_testing_face.h @@ -25,6 +25,17 @@ #ifndef THERMISTOR_TESTING_FACE_H_ #define THERMISTOR_TESTING_FACE_H_ +/* + * THERMISTOR TESTING FACE + * + * This watch face is designed for testing temperature sensor boards. + * It displays temperature readings at a relatively fast rate of 8 Hz, + * and disables low energy mode so my testing device doesn't sleep. + * You more than likely want to use thermistor_readout_face instead. + * + * Press ALARM to toggle display of metric vs. imperial units. + */ + #include "movement.h" void thermistor_testing_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/movement/watch_faces/settings/finetune_face.c b/movement/watch_faces/settings/finetune_face.c index 3b326b9..67680ed 100644 --- a/movement/watch_faces/settings/finetune_face.c +++ b/movement/watch_faces/settings/finetune_face.c @@ -20,21 +20,6 @@ * 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. - * - * FineTune face allows to align watch with sub-second precision in 25/250ms accuracy. - * Counts time since previous finetune, and allows to calculate & apply ppm correction for nanosec. - * - * Main screen - adjust delay (light/alarm) - * Long mode press - show hours since previous finetune - * Long mode press - show calculated ppm correction. You can apply it with long light, or just reset finetune timer with long alarm. - * - * Finetune will apply crystal aging correction on every finetune save (as aging is calculated since "last finetune" timestamp) - but you should worry - * about aging only on second/third years of watch calibration (if you are really looking at less than 10 seconds per year of error). - * - * Warning, do not use at the first second of a month, as you might stay at the same month and it will surprise you. - * Just wait 1 second...We are not fully replicating RTC timer behavior when RTC is off. - * Simulating months and years is... too much complexity. - * */ #include diff --git a/movement/watch_faces/settings/finetune_face.h b/movement/watch_faces/settings/finetune_face.h index 6a80bf2..95ac570 100644 --- a/movement/watch_faces/settings/finetune_face.h +++ b/movement/watch_faces/settings/finetune_face.h @@ -25,6 +25,34 @@ #ifndef FINETUNE_FACE_H_ #define FINETUNE_FACE_H_ +/* + * FINETUNE face + * + * FineTune face allows to align watch with sub-second precision in 25/250ms + * accuracy. Counts time since previous finetune, and allows to calculate & + * apply ppm correction for nanosec. + * + * Best used in conjunction with the NANOSEC face. + * + * Main screen - adjust delay (light/alarm) + * Long MODE press - show hours since previous finetune + * Long MODE press - show calculated ppm correction. + * You can apply it with long LIGHT, or just reset finetune timer with long ALARM. + * + * Finetune will apply crystal aging correction on every finetune save + * (as aging is calculated since "last finetune" timestamp); but you should + * worry about aging only on second/third years of watch calibration (if you + * are really looking at less than 10 seconds per year of error). + * + * Warning, do not use at the first second of a month, as you might stay at + * the same month and it will surprise you. Just wait 1 second...We are not + * fully replicating RTC timer behavior when RTC is off. + * Simulating months and years is... too much complexity. + * + * For full usage instructions, please refer to the wiki: + * https://www.sensorwatch.net/docs/watchfaces/nanosec/ + */ + #include "movement.h" typedef struct { diff --git a/movement/watch_faces/settings/nanosec_face.c b/movement/watch_faces/settings/nanosec_face.c index b9655c1..37dd08e 100644 --- a/movement/watch_faces/settings/nanosec_face.c +++ b/movement/watch_faces/settings/nanosec_face.c @@ -22,33 +22,6 @@ * SOFTWARE. */ -/* - * The goal of nanosec face is dramatic improvement of SensorWatch accuracy. - * Minimum goal is <60 seconds of error per year. Full success is if we can reach <15 seconds per year (<0.47ppm error). - * - * It implements temperature correction using tempco from datasheet (and allows to adjust these) - * and allows to introduce offset fix. Therefore requires temperature sensor board. - * - * Most users will need to apply profile 3 ("default") or 2("conservative datasheet"), and tune first parameter - - * static offset (as it's different for every crystal sample). - * - * Frequency correction is dithered over 31 correction intervals (31x10 minutes or ~5 hours), to allow <0.1ppm correction resolution. - * 1ppm is 0.0864 sec per day. - * 0.1ppm is 0.00864 sec per day. - * - * To stay under 1ppm error you would need calibration of your specific instance of quartz crystal after some "burn-in" (ideally 1 year). - * - * Should improve TOTP experience. - * - * Default funing fork tempco: -0.034 ppm/°C², centered around 25°C - * We add optional cubic coefficient, which was measured in practice on my sample. - * - * Cadence (CD) - how many minutes between corrections. Default 10 minutes. - * Every minute might be too much. Every hour - slightly less power consumption but also less precision. - * - * Can compensate crystal aging (ppm/year) - but you really should be worrying about it on second/third years of watch calibration. * - */ - #include #include #include @@ -272,7 +245,6 @@ static void value_increase(int16_t delta) { nanosec_state.correction_cadence = (delta > 0) ? 1 : 20; break; } - nanosec_state.correction_profile = (nanosec_state.correction_profile + delta) % nanosec_profile_count; break; case 6: // Aging nanosec_state.aging_ppm_pa += delta; diff --git a/movement/watch_faces/settings/nanosec_face.h b/movement/watch_faces/settings/nanosec_face.h index 044275a..545eed2 100644 --- a/movement/watch_faces/settings/nanosec_face.h +++ b/movement/watch_faces/settings/nanosec_face.h @@ -25,6 +25,47 @@ #ifndef NANOSEC_FACE_H_ #define NANOSEC_FACE_H_ +/* + * NANOSEC face + * + * The goal of nanosec face is dramatic improvement of SensorWatch accuracy. + * Minimum goal is <60 seconds of error per year. Full success is if we can + * reach <15 seconds per year (<0.47ppm error). + * + * Best used in conjunction with the FINETUNE face. + * + * It implements temperature correction using tempco from datasheet (and + * allows to adjust these) and allows to introduce offset fix. Therefore + * requires temperature sensor board. + * + * Most users will need to apply profile 3 ("default") or 2 ("conservative + * datasheet"), and tune first parameter "static offset" (as it's different + * for every crystal sample). + * + * Frequency correction is dithered over 31 correction intervals (31x10 + * minutes or ~5 hours), to allow <0.1ppm correction resolution. + * * 1ppm is 0.0864 sec per day. + * * 0.1ppm is 0.00864 sec per day. + * + * To stay under 1ppm error you would need calibration of your specific + * instance of quartz crystal after some "burn-in" (ideally 1 year). + * + * Should improve TOTP experience. + * + * Default funing fork tempco: -0.034 ppm/°C², centered around 25°C + * We add optional cubic coefficient, which was measured in practice on my sample. + * + * Cadence (CD) - how many minutes between corrections. Default 10 minutes. + * Every minute might be too much. Every hour - slightly less power + * consumption but also less precision. + * + * Can compensate crystal aging (ppm/year) - but you really should be worrying + * about it on second/third years of watch calibration. + * + * For full usage instructions, please refer to the wiki: + * https://www.sensorwatch.net/docs/watchfaces/nanosec/ + */ + #include "movement.h" #define nanosec_profile_count 5 diff --git a/movement/watch_faces/settings/preferences_face.c b/movement/watch_faces/settings/preferences_face.c index b0e328b..c96e8d1 100644 --- a/movement/watch_faces/settings/preferences_face.c +++ b/movement/watch_faces/settings/preferences_face.c @@ -136,22 +136,22 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings watch_display_string(" Never", 4); break; case 1: - watch_display_string("1 hour", 4); + watch_display_string("10n&in", 4); break; case 2: - watch_display_string("2 hour", 4); + watch_display_string("1 hour", 4); break; case 3: - watch_display_string("6 hour", 4); + watch_display_string("2 hour", 4); break; case 4: - watch_display_string("12 hr", 4); + watch_display_string("6 hour", 4); break; case 5: - watch_display_string(" 1 day", 4); + watch_display_string("12 hr", 4); break; case 6: - watch_display_string(" 2 day", 4); + watch_display_string(" 1 day", 4); break; case 7: watch_display_string(" 7 day", 4); diff --git a/movement/watch_faces/settings/preferences_face.h b/movement/watch_faces/settings/preferences_face.h index b178bfd..c8f36d7 100644 --- a/movement/watch_faces/settings/preferences_face.h +++ b/movement/watch_faces/settings/preferences_face.h @@ -25,6 +25,57 @@ #ifndef PREFERENCES_FACE_H_ #define PREFERENCES_FACE_H_ +/* + * PREFERENCES face + * + * The Preferences watch face allows you to configure various options on your + * Sensor Watch. Like all other screens, you advance the field you’re setting + * with the Light button, and advance its value with the Alarm button. The + * Preferences watch face labels each setting with a two-letter code on the + * top row; the following list describes each setting and their options: + * + * CL - Clock mode. + * This setting allows you to select a 12-or 24-hour clock display. All + * watch faces that support displaying the time will respect this setting; + * for example, both Simple Clock, World Clock and Sunrise/Sunset will + * display the time in 24 hour format if the 24 hour clock is selected here. + * + * BT - Button tone. + * This setting is only relevant if you installed the buzzer connector, + * and it toggles the beep when changing modes. If Y, the buzzer will + * sound a tone when Mode is pressed. Change to N to make the Mode + * button silent. + * + * TO - Timeout. + * Sets the time until screens that time out (like Settings and Time Set) + * snap back to the first screen. 60 seconds is a good default for the + * stock firmware, but if you choose a custom firmware with faces that + * you’d like to keep on screen for longer, you can set that here. + * + * LE - Low Energy mode. + * Sets the time until the watch enters its low energy sleep mode. + * Options range from 1 hour to 7 days, or Never. The more often Sensor + * Watch goes to sleep, the longer its battery will last — but you will + * lose the seconds indicator while it is asleep. This setting allows + * you to make a tradeoff between the device’s responsiveness and its + * longevity. + * + * LT - Light. + * This setting has three screens. + * The first lets you choose how long the LED should stay lit when the + * LIGHT button is pressed. Options are 1 second, 3 seconds and 5 + * seconds, or “No LED” to disable the LED entirely. + * The second screen, titled “blu” or “grn”, sets the intensity of the + * blue or green LED depending on the target Sensor Board hardware. + * Values range from 0 (off) to 15 (full intensity). + * The third screen, “red”, sets the intensity of the red LED, again + * from 0 to 15. + * On the last two screens, the LED remains on so that you can see the + * effect of mixing the two LED colors. On the Special Edition boards, + * you’ll have red, blue and a variety of shades of pink and purple to + * experiment with! + */ + #include "movement.h" void preferences_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/movement/watch_faces/settings/save_load_face.c b/movement/watch_faces/settings/save_load_face.c new file mode 100644 index 0000000..7b4be4d --- /dev/null +++ b/movement/watch_faces/settings/save_load_face.c @@ -0,0 +1,152 @@ +/* + * 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 "save_load_face.h" +#include "filesystem.h" + +static void save(save_load_state_t *state) { + savefile_t savefile = { + 1, + watch_get_backup_data(0), + watch_get_backup_data(1), + watch_get_backup_data(2), + watch_get_backup_data(3), + watch_get_backup_data(4), + watch_get_backup_data(5), + watch_get_backup_data(6), + watch_get_backup_data(7), + watch_rtc_get_date_time(), + }; + state->slot[state->index] = savefile; + char filename[23]; + sprintf(filename, "save_load_face_%d.bin", state->index); + filesystem_write_file(filename, (char*)&savefile, sizeof(savefile_t)); +} + +static void load(save_load_state_t *state, movement_settings_t *settings) { + watch_store_backup_data(state->slot[state->index].b0, 0); + settings->reg = state->slot[state->index].b0; + watch_store_backup_data(state->slot[state->index].b1, 1); + watch_store_backup_data(state->slot[state->index].b2, 2); + watch_store_backup_data(state->slot[state->index].b3, 3); + watch_store_backup_data(state->slot[state->index].b4, 4); + watch_store_backup_data(state->slot[state->index].b5, 5); + watch_store_backup_data(state->slot[state->index].b6, 6); + watch_store_backup_data(state->slot[state->index].b7, 7); +} + +static void load_saves_to_state(save_load_state_t *state) { + for (uint8_t i = 0; i < SAVE_LOAD_SLOTS; i++) { + char filename[23]; + sprintf(filename, "save_load_face_%d.bin", i); + if (filesystem_get_file_size(filename) != sizeof(savefile_t)) { + state->slot[i].version = 0; + continue; + } + filesystem_read_file(filename, (char*)&state->slot[i], sizeof(savefile_t)); + if (state->slot[i].version != 1) { + state->slot[i].version = 0; + } + } +} + +void save_load_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(save_load_state_t)); + memset(*context_ptr, 0, sizeof(save_load_state_t)); + } +} + +void save_load_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + save_load_state_t *state = (save_load_state_t *)context; + state->index = 0; + state->update_timeout = 0; + load_saves_to_state(state); +} + +static void update_display(save_load_state_t *state) { + char buf[11]; + sprintf(buf, "SL %1d", state->index); + watch_display_string(buf, 0); + + if (state->slot[state->index].version) { + sprintf(buf, "%02d%02d%02d", state->slot[state->index].rtc.unit.year + 20, state->slot[state->index].rtc.unit.month, state->slot[state->index].rtc.unit.day); + watch_display_string(buf, 4); + } else { + watch_display_string("no dat", 4); + } +} + +bool save_load_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + save_load_state_t *state = (save_load_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + update_display(state); + break; + case EVENT_TICK: + if (state->update_timeout) { + if (!--state->update_timeout) { + update_display(state); + } + } + break; + case EVENT_ALARM_BUTTON_UP: + state->index = (state->index + 1) % SAVE_LOAD_SLOTS; + update_display(state); + break; + case EVENT_LIGHT_LONG_PRESS: + save(state); + watch_display_string("Saved ", 4); + state->update_timeout = 3; + break; + case EVENT_ALARM_LONG_PRESS: + if (state->slot[state->index].version) { + load(state, settings); + watch_display_string("Loaded", 4); + state->update_timeout = 3; + } + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void save_load_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/settings/save_load_face.h b/movement/watch_faces/settings/save_load_face.h new file mode 100644 index 0000000..fc15531 --- /dev/null +++ b/movement/watch_faces/settings/save_load_face.h @@ -0,0 +1,82 @@ +/* + * 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 SAVE_LOAD_FACE_H_ +#define SAVE_LOAD_FACE_H_ + +#include "movement.h" +#include "watch_rtc.h" + +/* + * Save/Load face + * + * This allows you to save your settings (including location, birthday, etc) to + * LFS, which is not wiped when firmware is updated, and then load them again. + * It provides multiple save slots (four by default). + * + * Press ALARM to cycle through save slots. Long press LIGHT to save to the + * current slot. Long press ALARM to load from the current slot. The date that + * a save was taken is shown in YYMMDD format , or "no dat" if the slot is + * empty. The index of the current slot is displayed in the upper right corner. + * + * While the time that a save was taken is recorded, it is not currently + * restored. + */ + +typedef struct savefile { + uint8_t version; + uint32_t b0; + uint32_t b1; + uint32_t b2; + uint32_t b3; + uint32_t b4; + uint32_t b5; + uint32_t b6; + uint32_t b7; + watch_date_time rtc; +} savefile_t; + +#define SAVE_LOAD_SLOTS 4 + +typedef struct { + uint8_t index; + uint8_t update_timeout; + savefile_t slot[SAVE_LOAD_SLOTS]; +} save_load_state_t; + +void save_load_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void save_load_face_activate(movement_settings_t *settings, void *context); +bool save_load_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void save_load_face_resign(movement_settings_t *settings, void *context); + +#define save_load_face ((const watch_face_t){ \ + save_load_face_setup, \ + save_load_face_activate, \ + save_load_face_loop, \ + save_load_face_resign, \ + NULL, \ +}) + +#endif // SAVE_LOAD_FACE_H_ + diff --git a/movement/watch_faces/settings/set_time_face.h b/movement/watch_faces/settings/set_time_face.h index c86b637..f66dc9e 100644 --- a/movement/watch_faces/settings/set_time_face.h +++ b/movement/watch_faces/settings/set_time_face.h @@ -25,6 +25,23 @@ #ifndef SET_TIME_FACE_H_ #define SET_TIME_FACE_H_ +/* + * SET TIME face + * + * The default method for adjusting Sensor Watch time. + * + * The Time Set watch face allows you to set the time on Sensor Watch. Use + * the LIGHT button to advance through the field you are setting, and the + * ALARM button to change the value in that field. The fields are, in order: + * Hour, Minute, Second, Year, Month, Day and Time Zone. + * + * For features like World Clock and Sunrise/Sunset to work correctly, you + * must set the time to your local time, and the time zone to your local time + * zone. This allows Sensor Watch to correctly offset the time. This also + * means that when daylight savings time starts or ends, you must update + * both the time and the time zone on this screen. + */ + #include "movement.h" void set_time_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/movement/watch_faces/settings/set_time_hackwatch_face.c b/movement/watch_faces/settings/set_time_hackwatch_face.c index c65e2ff..fbe8cbb 100644 --- a/movement/watch_faces/settings/set_time_hackwatch_face.c +++ b/movement/watch_faces/settings/set_time_hackwatch_face.c @@ -21,21 +21,6 @@ * 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 is an extended version of set_time face which allow setting seconds precisely. - * To achieve that - press and hold alarm button few seconds before 00 and release exaclty as reference clock turns 00. - * All settings can go up, or down (long alarm press). - * - * The challenge is that SensorWatch display is delayed 0.5 seconds vs hardware RTC clock. It is caused by interrupts being generated by raising - * edge of counter. It means there is no way to precisely trigger at 0.5s, as events at different frequencies slightly mismatch. - * This watch face achieves this approximately by triggering at 15th out of 32Hz events. - * - * If you are <30 seconds when setting seconds - you will stay in the same minute. Otherwise - you will go to next minute. - * - * Note that changing anything will slightly delay subseconds counter. This is why this face sets seconds last - * to achiveve best precision. Still, best possible precision is achieved with finetune face. */ #include diff --git a/movement/watch_faces/settings/set_time_hackwatch_face.h b/movement/watch_faces/settings/set_time_hackwatch_face.h index 6d82066..41b91ac 100644 --- a/movement/watch_faces/settings/set_time_hackwatch_face.h +++ b/movement/watch_faces/settings/set_time_hackwatch_face.h @@ -25,6 +25,29 @@ #ifndef SET_TIME_HACKWATCH_FACE_H_ #define SET_TIME_HACKWATCH_FACE_H_ +/* + * SET TIME HACKWATCH + * + * This is an extended version of set_time face which allow setting seconds + * precisely. To achieve that - press and hold alarm button few seconds before + * 00 and release exaclty as reference clock turns 00. + * + * All settings can go up, or down (long alarm press). + * + * The challenge is that SensorWatch display is delayed 0.5 seconds vs hardware + * RTC clock. It is caused by interrupts being generated by raising edge of + * counter. It means there is no way to precisely trigger at 0.5s, as events + * at different frequencies slightly mismatch. This watch face achieves this + * approximately by triggering at 15th out of 32Hz events. + * + * If you are <30 seconds when setting seconds - you will stay in the same + * minute. Otherwise - you will go to next minute. + * + * Note that changing anything will slightly delay subseconds counter. This + * is why this face sets seconds last to achiveve best precision. Still, + * best possible precision is achieved with finetune face. + */ + #include "movement.h" void set_time_hackwatch_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); diff --git a/openocd.cfg b/openocd.cfg new file mode 100644 index 0000000..427bbc6 --- /dev/null +++ b/openocd.cfg @@ -0,0 +1,10 @@ +adapter driver cmsis-dap +adapter speed 5000 +transport select swd + +swd newdap SAML22 cpu -irlen 4 +dap create SAML22.dap -chain-position SAML22.cpu + +set _TARGETNAME SAML22.cpu +target create $_TARGETNAME cortex_m -dap SAML22.dap +cortex_m reset_config sysresetreq diff --git a/utils/movement_bulk_installer/standard-red.uf2 b/utils/movement_bulk_installer/standard-red.uf2 index 385c611..b74e539 100644 Binary files a/utils/movement_bulk_installer/standard-red.uf2 and b/utils/movement_bulk_installer/standard-red.uf2 differ diff --git a/utils/uf2conv.py b/utils/uf2conv.py index 849e649..7263cba 100644 --- a/utils/uf2conv.py +++ b/utils/uf2conv.py @@ -180,7 +180,7 @@ def get_drives(): "get", "DeviceID,", "VolumeName,", "FileSystem,", "DriveType"]) for line in to_str(r).split('\n'): - words = re.split('\s+', line) + words = re.split(r'\s+', line) if len(words) >= 3 and words[1] == "2" and words[2] == "FAT": drives.append(words[0]) else: diff --git a/watch-library/hardware/hal/include/hpl_sleep.h b/watch-library/hardware/hal/include/hpl_sleep.h index 6731ec3..4106fb7 100644 --- a/watch-library/hardware/hal/include/hpl_sleep.h +++ b/watch-library/hardware/hal/include/hpl_sleep.h @@ -70,6 +70,16 @@ extern "C" { */ int32_t _set_sleep_mode(const uint8_t mode); +/** + * \brief Get the sleep mode for the device + * + * This function gets the sleep mode for the device. + * + * \return the current value of the sleep mode configuration bits + */ +int32_t _get_sleep_mode(void); + + /** * \brief Reset MCU */ diff --git a/watch-library/hardware/hal/src/hal_sleep.c b/watch-library/hardware/hal/src/hal_sleep.c index 89472f1..2fac64d 100644 --- a/watch-library/hardware/hal/src/hal_sleep.c +++ b/watch-library/hardware/hal/src/hal_sleep.c @@ -57,6 +57,15 @@ int sleep(const uint8_t mode) if (ERR_NONE != _set_sleep_mode(mode)) return ERR_INVALID_ARG; + // wait for the mode set to actually take, per note in Microchip data + // sheet DS60001465, section 19.8.2: + // + // A small latency happens between the store instruction and actual + // writing of the SLEEPCFG register due to bridges. Software has to make + // sure the SLEEPCFG register reads the wanted value before issuing WFI + // instruction. + while(_get_sleep_mode() != mode); + _go_to_sleep(); return ERR_NONE; diff --git a/watch-library/hardware/hpl/pm/hpl_pm.c b/watch-library/hardware/hpl/pm/hpl_pm.c index d6439f1..2e9e37b 100644 --- a/watch-library/hardware/hpl/pm/hpl_pm.c +++ b/watch-library/hardware/hpl/pm/hpl_pm.c @@ -63,6 +63,14 @@ int32_t _set_sleep_mode(const uint8_t mode) return ERR_NONE; } +/** + * \brief Get the sleep mode for the device + */ +int32_t _get_sleep_mode() +{ + return hri_pm_read_SLEEPCFG_SLEEPMODE_bf(PM); +} + /** * \brief Set performance level */ diff --git a/watch-library/hardware/main.c b/watch-library/hardware/main.c index 325610f..8ac4fca 100755 --- a/watch-library/hardware/main.c +++ b/watch-library/hardware/main.c @@ -79,7 +79,6 @@ int main(void) { while (1) { bool usb_enabled = hri_usbdevice_get_CTRLA_ENABLE_bit(USB); bool can_sleep = app_loop(); - if (can_sleep && !usb_enabled) { app_prepare_for_standby(); sleep(4); diff --git a/watch-library/hardware/startup_saml22.c b/watch-library/hardware/startup_saml22.c index f498256..2d2027f 100755 --- a/watch-library/hardware/startup_saml22.c +++ b/watch-library/hardware/startup_saml22.c @@ -220,6 +220,5 @@ void Reset_Handler(void) */ void Dummy_Handler(void) { - while (1) { - } + NVIC_SystemReset(); } diff --git a/watch-library/hardware/watch/watch_buzzer.c b/watch-library/hardware/watch/watch_buzzer.c index 18fb4db..2dce8d2 100644 --- a/watch-library/hardware/watch/watch_buzzer.c +++ b/watch-library/hardware/watch/watch_buzzer.c @@ -23,6 +23,7 @@ */ #include "watch_buzzer.h" +#include "watch_private_buzzer.h" #include "../../../watch-library/hardware/include/saml22j18a.h" #include "../../../watch-library/hardware/include/component/tc.h" #include "../../../watch-library/hardware/hri/hri_tc_l22.h" diff --git a/watch-library/hardware/watch/watch_deepsleep.c b/watch-library/hardware/watch/watch_deepsleep.c index ae2ad31..efdad6d 100644 --- a/watch-library/hardware/watch/watch_deepsleep.c +++ b/watch-library/hardware/watch/watch_deepsleep.c @@ -22,6 +22,8 @@ * SOFTWARE. */ +#include "hpl_systick_config.h" + #include "watch_extint.h" // this warning only appears when you `make BOARD=OSO-SWAT-A1-02`. it's annoying, @@ -158,14 +160,20 @@ void watch_enter_sleep_mode(void) { // disable brownout detector interrupt, which could inadvertently wake us up. SUPC->INTENCLR.bit.BOD33DET = 1; + // per Microchip datasheet clarification DS80000782, + // work around silicon erratum 1.8.4 by disabling the SysTick interrupt, which is + // enabled as part of driver init, before going to sleep. + SysTick->CTRL = SysTick->CTRL & ~(CONF_SYSTICK_TICKINT << SysTick_CTRL_TICKINT_Pos); + // disable all pins _watch_disable_all_pins_except_rtc(); // enter standby (4); we basically hang out here until an interrupt wakes us. sleep(4); - // and we awake! re-enable the brownout detector + // and we awake! re-enable the brownout detector and SysTick interrupt SUPC->INTENSET.bit.BOD33DET = 1; + SysTick->CTRL = SysTick->CTRL | (CONF_SYSTICK_TICKINT << SysTick_CTRL_TICKINT_Pos); // call app_setup so the app can re-enable everything we disabled. app_setup(); diff --git a/watch-library/hardware/watch/watch_private.c b/watch-library/hardware/watch/watch_private.c index cd607b8..4de9713 100644 --- a/watch-library/hardware/watch/watch_private.c +++ b/watch-library/hardware/watch/watch_private.c @@ -23,6 +23,7 @@ */ #include "watch_private.h" +#include "watch_private_cdc.h" #include "watch_utility.h" #include "tusb.h" @@ -35,6 +36,12 @@ void _watch_init(void) { // Use switching regulator for lower power consumption. SUPC->VREG.bit.SEL = 1; + + // per Microchip datasheet clarification DS80000782, + // work around silicon erratum 1.7.2, which causes the microcontroller to lock up on leaving standby: + // request that the voltage regulator run in standby, and also that it switch to PL0. + SUPC->VREG.bit.RUNSTDBY = 1; + SUPC->VREG.bit.STDBYPL0 = 1; while(!SUPC->STATUS.bit.VREGRDY); // wait for voltage regulator to become ready // check the battery voltage... @@ -106,12 +113,21 @@ int getentropy(void *buf, size_t buflen) { } } - hri_trng_clear_CTRLA_ENABLE_bit(TRNG); + watch_disable_TRNG(); hri_mclk_clear_APBCMASK_TRNG_bit(MCLK); return 0; } +void watch_disable_TRNG() { + // per Microchip datasheet clarification DS80000782, + // silicon erratum 1.16.1 indicates that the TRNG may leave internal components powered after being disabled. + // the workaround is to disable the TRNG by clearing the control register, twice. + hri_trng_write_CTRLA_reg(TRNG, 0); + hri_trng_write_CTRLA_reg(TRNG, 0); +} + + void _watch_enable_tcc(void) { // clock TCC0 with the main clock (8 MHz) and enable the peripheral clock. hri_gclk_write_PCHCTRL_reg(GCLK, TCC0_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK0_Val | GCLK_PCHCTRL_CHEN); @@ -170,6 +186,87 @@ void _watch_disable_tcc(void) { // disable the TCC hri_tcc_clear_CTRLA_ENABLE_bit(TCC0); hri_mclk_clear_APBCMASK_TCC0_bit(MCLK); +} + +void _watch_enable_tc0(void) { + // before we init TinyUSB, we are going to need a periodic callback to handle TinyUSB tasks. + // TC2 and TC3 are reserved for devices on the 9-pin connector, so let's use TC0. + // clock TC0 with the 8 MHz clock on GCLK0. + hri_gclk_write_PCHCTRL_reg(GCLK, TC0_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK0_Val | GCLK_PCHCTRL_CHEN); + // and enable the peripheral clock. + hri_mclk_set_APBCMASK_TC0_bit(MCLK); + // disable and reset TC0. + hri_tc_clear_CTRLA_ENABLE_bit(TC0); + hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_ENABLE); + hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_SWRST); + hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_SWRST); + hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_PRESCALER_DIV1024 | // divide the 8 MHz clock by 1024 to count at 7812.5 Hz + TC_CTRLA_MODE_COUNT8 | // count in 8-bit mode + TC_CTRLA_RUNSTDBY); // run in standby, just in case we figure that out + hri_tccount8_write_PER_reg(TC0, 10); // 7812.5 Hz / 10 = 781.125 Hz + // set an interrupt on overflow; this will call TC0_Handler below. + hri_tc_set_INTEN_OVF_bit(TC0); + + // set priority higher than TC1 + NVIC_SetPriority(TC0_IRQn, 5); + NVIC_ClearPendingIRQ(TC0_IRQn); + NVIC_EnableIRQ(TC0_IRQn); + + // Start the timer + hri_tc_set_CTRLA_ENABLE_bit(TC0); +} + +void _watch_disable_tc0(void) { + NVIC_DisableIRQ(TC0_IRQn); + NVIC_ClearPendingIRQ(TC0_IRQn); + hri_tc_clear_CTRLA_ENABLE_bit(TC0); + hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_ENABLE); + hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_SWRST); + hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_SWRST); +} + +void _watch_enable_tc1(void) { + hri_gclk_write_PCHCTRL_reg(GCLK, TC1_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK0_Val | GCLK_PCHCTRL_CHEN); + // and enable the peripheral clock. + hri_mclk_set_APBCMASK_TC1_bit(MCLK); + // disable and reset TC1. + hri_tc_clear_CTRLA_ENABLE_bit(TC1); + hri_tc_wait_for_sync(TC1, TC_SYNCBUSY_ENABLE); + hri_tc_write_CTRLA_reg(TC1, TC_CTRLA_SWRST); + hri_tc_wait_for_sync(TC1, TC_SYNCBUSY_SWRST); + hri_tc_write_CTRLA_reg(TC1, TC_CTRLA_PRESCALER_DIV1024 | // divide the 8 MHz clock by 1024 to count at 7812.5 Hz + TC_CTRLA_MODE_COUNT8 | // count in 8-bit mode + TC_CTRLA_RUNSTDBY); // run in standby, just in case we figure that out + hri_tccount8_write_PER_reg(TC1, 20); // 7812.5 Hz / 50 = 156.25 Hz + // set an interrupt on overflow; this will call TC1_Handler below. + hri_tc_set_INTEN_OVF_bit(TC1); + + // set priority lower than TC0 + NVIC_SetPriority(TC1_IRQn, 6); + NVIC_ClearPendingIRQ(TC1_IRQn); + NVIC_EnableIRQ(TC1_IRQn); + + // Start the timer + hri_tc_set_CTRLA_ENABLE_bit(TC1); +} + +void _watch_disable_tc1(void) { + NVIC_DisableIRQ(TC1_IRQn); + NVIC_ClearPendingIRQ(TC1_IRQn); + hri_tc_clear_CTRLA_ENABLE_bit(TC1); + hri_tc_wait_for_sync(TC1, TC_SYNCBUSY_ENABLE); + hri_tc_write_CTRLA_reg(TC1, TC_CTRLA_SWRST); + hri_tc_wait_for_sync(TC1, TC_SYNCBUSY_SWRST); +} + +void TC0_Handler(void) { + tud_task(); + TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; +} + +void TC1_Handler(void) { + cdc_task(); + TC1->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; } void _watch_enable_usb(void) { @@ -216,76 +313,17 @@ void _watch_enable_usb(void) { gpio_set_pin_function(PIN_PA24, PINMUX_PA24G_USB_DM); gpio_set_pin_function(PIN_PA25, PINMUX_PA25G_USB_DP); - // before we init TinyUSB, we are going to need a periodic callback to handle TinyUSB tasks. - // TC2 and TC3 are reserved for devices on the 9-pin connector, so let's use TC0. - // clock TC0 with the 8 MHz clock on GCLK0. - hri_gclk_write_PCHCTRL_reg(GCLK, TC0_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK0_Val | GCLK_PCHCTRL_CHEN); - // and enable the peripheral clock. - hri_mclk_set_APBCMASK_TC0_bit(MCLK); - // disable and reset TC0. - hri_tc_clear_CTRLA_ENABLE_bit(TC0); - hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_ENABLE); - hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_SWRST); - hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_SWRST); - // configure the TC to overflow 1,000 times per second - hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_PRESCALER_DIV64 | // divide the 8 MHz clock by 64 to count at 125 KHz - TC_CTRLA_MODE_COUNT8 | // count in 8-bit mode - TC_CTRLA_RUNSTDBY); // run in standby, just in case we figure that out - hri_tccount8_write_PER_reg(TC0, 125); // 125000 Hz / 125 = 1,000 Hz - // set an interrupt on overflow; this will call TC0_Handler below. - hri_tc_set_INTEN_OVF_bit(TC0); - NVIC_ClearPendingIRQ(TC0_IRQn); - NVIC_EnableIRQ (TC0_IRQn); + _watch_enable_tc0(); - // now we can init TinyUSB tusb_init(); - // and start the timer that handles USB device tasks. - hri_tc_set_CTRLA_ENABLE_bit(TC0); -} -// this function ends up getting called by printf to log stuff to the USB console. -int _write(int file, char *ptr, int len) { - (void)file; - if (hri_usbdevice_get_CTRLA_ENABLE_bit(USB)) { - tud_cdc_n_write(0, (void const*)ptr, len); - tud_cdc_n_write_flush(0); - return len; - } - - return 0; -} - -static char buf[256] = {0}; - -int _read(int file, char *ptr, int len) { - (void)file; - int actual_length = strlen(buf); - if (actual_length) { - memcpy(ptr, buf, min(len, actual_length)); - return actual_length; - } - return 0; + _watch_enable_tc1(); } void USB_Handler(void) { tud_int_handler(0); } -static void cdc_task(void) { - if (tud_cdc_n_available(0)) { - tud_cdc_n_read(0, buf, sizeof(buf)); - } else { - memset(buf, 0, 256); - } -} - -void TC0_Handler(void) { - tud_task(); - cdc_task(); - TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; -} - - // USB Descriptors and tinyUSB callbacks follow. /* diff --git a/watch-library/hardware/watch/watch_private_cdc.c b/watch-library/hardware/watch/watch_private_cdc.c new file mode 100644 index 0000000..a961b5e --- /dev/null +++ b/watch-library/hardware/watch/watch_private_cdc.c @@ -0,0 +1,160 @@ +/* + * MIT License + * + * Copyright (c) 2020 Joey Castillo + * Copyright (c) 2023 Edward Shin + * + * 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 "watch_private_cdc.h" + +#include + +#include "watch_utility.h" +#include "tusb.h" + +/* + * Implement a circular buffer for the USB CDC Serial read buffer. + * The size of the buffer must be a power of two for this circular buffer + * implementation to work. + */ + +// Size of the circular buffer. Must be a power of two. +#define CDC_WRITE_BUF_SZ (1024) +// Macro function to perform modular arithmetic on an index. +// eg. (63 + 2) & (64 - 1) -> 1 +#define CDC_WRITE_BUF_IDX(x) ((x) & (CDC_WRITE_BUF_SZ - 1)) +static char s_write_buf[CDC_WRITE_BUF_SZ] = {0}; +static size_t s_write_buf_pos = 0; +static size_t s_write_buf_len = 0; + +#define CDC_READ_BUF_SZ (256) +#define CDC_READ_BUF_IDX(x) ((x) & (CDC_READ_BUF_SZ - 1)) +static char s_read_buf[CDC_READ_BUF_SZ] = {0}; +static size_t s_read_buf_pos = 0; +static size_t s_read_buf_len = 0; + +// Mask TC1 interrupts, preventing calls to cdc_task() +static inline void prv_critical_section_enter(void) { + NVIC_DisableIRQ(TC1_IRQn); +} + +// Unmask TC1 interrupts, allowing calls to cdc_task() +static inline void prv_critical_section_exit(void) { + NVIC_EnableIRQ(TC1_IRQn); +} + +int _write(int file, char *ptr, int len) { + (void) file; + + if (ptr == NULL || len <= 0) { + return -1; + } + + int bytes_written = 0; + + prv_critical_section_enter(); + + for (int i = 0; i < len; i++) { + s_write_buf[s_write_buf_pos] = ptr[i]; + s_write_buf_pos = CDC_WRITE_BUF_IDX(s_write_buf_pos + 1); + if (s_write_buf_len < CDC_WRITE_BUF_SZ) { + s_write_buf_len++; + } + bytes_written++; + } + + prv_critical_section_exit(); + + return bytes_written; +} + +int _read(int file, char *ptr, int len) { + (void) file; + + prv_critical_section_enter(); + + if (ptr == NULL || len <= 0 || s_read_buf_len == 0) { + prv_critical_section_exit(); + return -1; + } + + // Clamp to the length of the read buffer + if ((size_t) len > s_read_buf_len) { + len = s_read_buf_len; + } + + // Calculate the start of the circular buffer, and iterate from there + const size_t start_pos = CDC_READ_BUF_IDX(s_read_buf_pos - len); + for (size_t i = 0; i < (size_t) len; i++) { + const size_t idx = CDC_READ_BUF_IDX(start_pos + i); + ptr[i] = s_read_buf[idx]; + s_read_buf[idx] = 0; + } + + // Update circular buffer position and length + s_read_buf_len -= len; + s_read_buf_pos = CDC_READ_BUF_IDX(s_read_buf_pos - len); + + prv_critical_section_exit(); + + return len; +} + +static void prv_handle_reads(void) { + while (tud_cdc_available()) { + int c = tud_cdc_read_char(); + if (c < 0) { + continue; + } + s_read_buf[s_read_buf_pos] = c; + s_read_buf_pos = CDC_READ_BUF_IDX(s_read_buf_pos + 1); + if (s_read_buf_len < CDC_READ_BUF_SZ) { + s_read_buf_len++; + } + } +} + +static void prv_handle_writes(void) { + if (s_write_buf_len > 0) { + const size_t start_pos = + CDC_WRITE_BUF_IDX(s_write_buf_pos - s_write_buf_len); + for (size_t i = 0; i < (size_t) s_write_buf_len; i++) { + const size_t idx = CDC_WRITE_BUF_IDX(start_pos + i); + if (tud_cdc_available() > 0) { + // If we receive data while doing a large write, we need to + // fully service it before continuing to write, or the + // stack will crash. + prv_handle_reads(); + } + if (tud_cdc_write_available()) { + tud_cdc_write(&s_write_buf[idx], 1); + } + s_write_buf[idx] = 0; + s_write_buf_len--; + } + tud_cdc_write_flush(); + } +} + +void cdc_task(void) { + prv_handle_reads(); + prv_handle_writes(); +} diff --git a/watch-library/hardware/watch/watch_private_cdc.h b/watch-library/hardware/watch/watch_private_cdc.h new file mode 100644 index 0000000..b7fa958 --- /dev/null +++ b/watch-library/hardware/watch/watch_private_cdc.h @@ -0,0 +1,33 @@ +/* + * MIT License + * + * Copyright (c) 2020 Joey Castillo + * Copyright (c) 2023 Edward Shin + * + * 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 _WATCH_PRIVATE_CDC_H_INCLUDED +#define _WATCH_PRIVATE_CDC_H_INCLUDED + +int _write(int file, char *ptr, int len); +int _read(int file, char *ptr, int len); +void cdc_task(void); + +#endif diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 881e257..93cb9f1 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -84,7 +84,7 @@ void watch_rtc_register_periodic_callback(ext_irq_cb_t callback, uint8_t frequen if (__builtin_popcount(frequency) != 1) return; // this left-justifies the period in a 32-bit integer. - uint32_t tmp = frequency << 24; + uint32_t tmp = (frequency & 0xFF) << 24; // now we can count the leading zeroes to get the value we need. // 0x01 (1 Hz) will have 7 leading zeros for PER7. 0xF0 (128 Hz) will have no leading zeroes for PER0. uint8_t per_n = __builtin_clz(tmp); @@ -99,7 +99,7 @@ void watch_rtc_register_periodic_callback(ext_irq_cb_t callback, uint8_t frequen void watch_rtc_disable_periodic_callback(uint8_t frequency) { if (__builtin_popcount(frequency) != 1) return; - uint8_t per_n = __builtin_clz(frequency << 24); + uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24); RTC->MODE2.INTENCLR.reg = 1 << per_n; } diff --git a/watch-library/shared/watch/watch.h b/watch-library/shared/watch/watch.h index 790f9a1..62e57a5 100644 --- a/watch-library/shared/watch/watch.h +++ b/watch-library/shared/watch/watch.h @@ -88,6 +88,10 @@ bool watch_is_usb_enabled(void); */ void watch_reset_to_bootloader(void); +/** @brief Call periodically from app main loop to service CDC RX/TX. + */ +void cdc_task(void); + /** @brief Reads up to len bytes from the USB serial. * @param file ignored, you can pass in 0 * @param ptr pointer to a buffer of at least len bytes @@ -96,4 +100,8 @@ void watch_reset_to_bootloader(void); */ int read(int file, char *ptr, int len); -#endif /* WATCH_H_ */ \ No newline at end of file +/** @brief Disables the TRNG twice in order to work around silicon erratum 1.16.1. + */ +void watch_disable_TRNG(); + +#endif /* WATCH_H_ */ diff --git a/watch-library/shared/watch/watch_buzzer.h b/watch-library/shared/watch/watch_buzzer.h index 7ba9a52..4c39475 100644 --- a/watch-library/shared/watch/watch_buzzer.h +++ b/watch-library/shared/watch/watch_buzzer.h @@ -175,6 +175,8 @@ extern const uint16_t NotePeriods[108]; */ void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)); +uint16_t sequence_length(int8_t *sequence); + /** @brief Aborts a playing sequence. */ void watch_buzzer_abort_sequence(void); diff --git a/watch-library/shared/watch/watch_private.h b/watch-library/shared/watch/watch_private.h index 9d55bc2..8fcc575 100644 --- a/watch-library/shared/watch/watch_private.h +++ b/watch-library/shared/watch/watch_private.h @@ -38,14 +38,19 @@ void _watch_enable_tcc(void); /// Called by buzzer and LED teardown functions. You should not call this from your app. void _watch_disable_tcc(void); +/// Enable USB task timer. Called by USB enable routine in main(). You should not call this from your app. +void _watch_enable_tc0(void); + +/// Disable USB task timer. You should not call this from your app. +void _watch_disable_tc0(void); + +/// Enable CDC task timer. Called by USB enable routine in main(). You should not call this from your app. +void _watch_enable_tc1(void); + +/// Disable CDC task timer. You should not call this from your app. +void _watch_disable_tc1(void); + /// Called by main.c if plugged in to USB. You should not call this from your app. void _watch_enable_usb(void); -// this function ends up getting called by printf to log stuff to the USB console. -int _write(int file, char *ptr, int len); - -// i thought this would be called by gets but it doesn't? anyway it does get called by read() -// so that's our mechanism for reading data from the USB serial console. -int _read(int file, char *ptr, int len); - #endif diff --git a/watch-library/shared/watch/watch_private_buzzer.c b/watch-library/shared/watch/watch_private_buzzer.c index 0618f42..def54a4 100644 --- a/watch-library/shared/watch/watch_private_buzzer.c +++ b/watch-library/shared/watch/watch_private_buzzer.c @@ -23,6 +23,13 @@ */ #include "driver_init.h" -// note: the buzzer uses a 1 MHz clock. these values were determined by dividing 1,000,000 by the target frequency. -// i.e. for a 440 Hz tone (A4 on the piano), 1MHz/440Hz = 2273 -const uint16_t NotePeriods[108] = {18182,17161,16197,15288,14430,13620,12857,12134,11453,10811,10204,9631,9091,8581,8099,7645,7216,6811,6428,6068,5727,5405,5102,4816,4545,4290,4050,3822,3608,3405,3214,3034,2863,2703,2551,2408,2273,2145,2025,1911,1804,1703,1607,1517,1432,1351,1276,1204,1136,1073,1012,956,902,851,804,758,716,676,638,602,568,536,506,478,451,426,402,379,358,338,319,301,284,268,253,239,225,213,201,190,179,169,159,150,142,134,127}; +uint16_t sequence_length(int8_t *sequence) { + uint16_t result = 0; + + while (*sequence != 0){ + result += *(sequence + 1); + sequence += 2; + } + + return result; +} diff --git a/watch-library/shared/watch/watch_private_buzzer.h b/watch-library/shared/watch/watch_private_buzzer.h new file mode 100644 index 0000000..3017bbb --- /dev/null +++ b/watch-library/shared/watch/watch_private_buzzer.h @@ -0,0 +1,33 @@ +/* + * 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 _WATCH_PRIVATE_BUZZER_H_INCLUDED +#define _WATCH_PRIVATE_BUZZER_H_INCLUDED + +// note: the buzzer uses a 1 MHz clock. these values were determined by dividing 1,000,000 by the target frequency. +// i.e. for a 440 Hz tone (A4 on the piano), 1MHz/440Hz = 2273 +const uint16_t NotePeriods[108] = {18182,17161,16197,15288,14430,13620,12857,12134,11453,10811,10204,9631,9091,8581,8099,7645,7216,6811,6428,6068,5727,5405,5102,4816,4545,4290,4050,3822,3608,3405,3214,3034,2863,2703,2551,2408,2273,2145,2025,1911,1804,1703,1607,1517,1432,1351,1276,1204,1136,1073,1012,956,902,851,804,758,716,676,638,602,568,536,506,478,451,426,402,379,358,338,319,301,284,268,253,239,225,213,201,190,179,169,159,150,142,134,127}; + +uint16_t sequence_length(int8_t *sequence); + +#endif diff --git a/watch-library/shared/watch/watch_private_display.c b/watch-library/shared/watch/watch_private_display.c index 245b20e..c12957d 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 } diff --git a/watch-library/shared/watch/watch_utility.c b/watch-library/shared/watch/watch_utility.c index 9e52476..64b3bb7 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; diff --git a/watch-library/simulator/shell.html b/watch-library/simulator/shell.html index 335b953..29fbed0 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,18 +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. +

+
{{{ SCRIPT }}} diff --git a/watch-library/simulator/watch/watch_buzzer.c b/watch-library/simulator/watch/watch_buzzer.c index 68d9a13..7ccb854 100644 --- a/watch-library/simulator/watch/watch_buzzer.c +++ b/watch-library/simulator/watch/watch_buzzer.c @@ -23,6 +23,7 @@ */ #include "watch_buzzer.h" +#include "watch_private_buzzer.h" #include "watch_main_loop.h" #include @@ -152,7 +153,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); } diff --git a/watch-library/simulator/watch/watch_extint.c b/watch-library/simulator/watch/watch_extint.c index cbba4c3..b5894b9 100644 --- a/watch-library/simulator/watch/watch_extint.c +++ b/watch-library/simulator/watch/watch_extint.c @@ -22,13 +22,15 @@ * SOFTWARE. */ +#include + #include "watch_extint.h" #include "watch_main_loop.h" #include #include -static bool output_focused = false; +static bool debug_console_focused = false; static bool external_interrupt_enabled = false; static bool button_callbacks_installed = false; static ext_irq_cb_t external_interrupt_mode_callback = NULL; @@ -45,27 +47,47 @@ static const uint8_t BTN_IDS[] = { BTN_ID_ALARM, BTN_ID_LIGHT, BTN_ID_MODE }; static EM_BOOL watch_invoke_interrupt_callback(const uint8_t button_id, watch_interrupt_trigger trigger); static EM_BOOL watch_invoke_key_callback(int eventType, const EmscriptenKeyboardEvent *keyEvent, void *userData) { - if (output_focused || keyEvent->repeat) return EM_FALSE; - - const char *key = keyEvent->key; - if (key[1] != 0) return EM_FALSE; + if (debug_console_focused || keyEvent->repeat) return EM_FALSE; uint8_t button_id; - switch (key[0]) { - case 'A': - case 'a': - button_id = BTN_ID_ALARM; - break; - case 'L': - case 'l': - button_id = BTN_ID_LIGHT; - break; - case 'M': - case 'm': - button_id = BTN_ID_MODE; - break; - default: - return EM_FALSE; + const char *key = keyEvent->key; + if (key[1] == 0) { + // event is from a plain letter key + switch (key[0]) { + case 'A': + case 'a': + button_id = BTN_ID_ALARM; + break; + case 'L': + case 'l': + button_id = BTN_ID_LIGHT; + break; + case 'M': + case 'm': + button_id = BTN_ID_MODE; + break; + default: + return EM_FALSE; + } + } else if (strncmp(key, "Arrow", 5) == 0) { + // event is from one of the arrow keys + switch(key[5]) { + case 'U': // ArrowUp + button_id = BTN_ID_LIGHT; + break; + case 'D': // ArrowDown + case 'L': // ArrowLeft + button_id = BTN_ID_MODE; + break; + case 'R': // ArrowRight + button_id = BTN_ID_ALARM; + break; + default: + return EM_FALSE; + } + } else { + // another kind of key + return EM_FALSE; } watch_interrupt_trigger trigger = eventType == EMSCRIPTEN_EVENT_KEYDOWN ? INTERRUPT_TRIGGER_RISING : INTERRUPT_TRIGGER_FALLING; @@ -86,7 +108,7 @@ static EM_BOOL watch_invoke_touch_callback(int eventType, const EmscriptenTouchE } static EM_BOOL watch_invoke_focus_callback(int eventType, const EmscriptenFocusEvent *focusEvent, void *userData) { - output_focused = eventType == EMSCRIPTEN_EVENT_FOCUS; + debug_console_focused = eventType == EMSCRIPTEN_EVENT_FOCUS; return EM_TRUE; } @@ -98,6 +120,10 @@ static void watch_install_button_callbacks(void) { emscripten_set_focus_callback(target_output, NULL, EM_FALSE, watch_invoke_focus_callback); emscripten_set_blur_callback(target_output, NULL, EM_FALSE, watch_invoke_focus_callback); + const char *target_input = "#input"; + emscripten_set_focus_callback(target_input, NULL, EM_FALSE, watch_invoke_focus_callback); + emscripten_set_blur_callback(target_input, NULL, EM_FALSE, watch_invoke_focus_callback); + for (int i = 0, count = sizeof(BTN_IDS) / sizeof(BTN_IDS[0]); i < count; i++) { char target[] = "#btn_"; target[4] = BTN_IDS[i] + '0'; diff --git a/watch-library/simulator/watch/watch_private.c b/watch-library/simulator/watch/watch_private.c index 3425341..03e1f08 100644 --- a/watch-library/simulator/watch/watch_private.c +++ b/watch-library/simulator/watch/watch_private.c @@ -57,6 +57,8 @@ void _watch_disable_tcc(void) {} void _watch_enable_usb(void) {} +void watch_disable_TRNG() {} + // this function ends up getting called by printf to log stuff to the USB console. int _write(int file, char *ptr, int len) { // TODO: (a2) hook to UI diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index fa80d6b..2bb6074 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -92,13 +92,12 @@ void watch_rtc_register_periodic_callback(ext_irq_cb_t callback, uint8_t frequen if (__builtin_popcount(frequency) != 1) return; // this left-justifies the period in a 32-bit integer. - uint32_t tmp = frequency << 24; + uint32_t tmp = (frequency & 0xFF) << 24; // now we can count the leading zeroes to get the value we need. // 0x01 (1 Hz) will have 7 leading zeros for PER7. 0xF0 (128 Hz) will have no leading zeroes for PER0. uint8_t per_n = __builtin_clz(tmp); - // this also maps nicely to an index for our list of tick callbacks. - double interval = 1000 / frequency; // in msec + double interval = 1000.0 / frequency; // in msec if (tick_callbacks[per_n] != -1) emscripten_clear_interval(tick_callbacks[per_n]); tick_callbacks[per_n] = emscripten_set_interval(watch_invoke_periodic_callback, interval, (void *)callback); @@ -106,7 +105,7 @@ void watch_rtc_register_periodic_callback(ext_irq_cb_t callback, uint8_t frequen void watch_rtc_disable_periodic_callback(uint8_t frequency) { if (__builtin_popcount(frequency) != 1) return; - uint8_t per_n = __builtin_clz(frequency << 24); + uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24); if (tick_callbacks[per_n] != -1) { emscripten_clear_interval(tick_callbacks[per_n]); tick_callbacks[per_n] = -1; @@ -115,7 +114,7 @@ void watch_rtc_disable_periodic_callback(uint8_t frequency) { void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) { for (int i = 0; i < 8; i++) { - if (tick_callbacks[i] != -1 && (mask & (1 << (7 - i))) != 0) { + if (tick_callbacks[i] != -1 && (mask & (1 << i)) != 0) { emscripten_clear_interval(tick_callbacks[i]); tick_callbacks[i] = -1; }