Merge remote-tracking branch 'origin/main' into manual_dst_toggle

* origin/main: (119 commits)
  add an openocd.cfg for openocd 0.12.0
  Fix compile errors and warnings in movement.c and shell.c
  faces/totp: avoid displaying when key is invalid
  faces/totp: fix error message not displayed bug
  faces/totp: remove dynamic memory allocation
  faces/totp: improve memory usage
  faces: restore simple_clock_face
  uf2conv: argument to `re.split` should be a rawstring
  movement: fix unintended timeout short circuiting
  movement: convert can_sleep an automatic variable
  faces/pulsometer: remember pulsometer measurement
  faces/pulsometer: remember pulsometer calibration
  faces/totp: update copyrights
  faces/totp: allow moving backwards through codes
  faces/clock: add 24h only feature
  faces/clock: update copyrights and credits
  faces/totp: delete leading underscores
  faces/totp: rename initializer macro to credential
  faces/totp: improve TOTP initializer labeling
  faces/totp: decode secrets when setting up
  ...
This commit is contained in:
R. Alex Barbieri 2024-04-27 13:23:53 -05:00
commit e3d67af604
171 changed files with 6015 additions and 1074 deletions

View file

@ -6,6 +6,9 @@ on:
branches-ignore:
- gh-pages
env:
COLOR: BLUE
jobs:
build:
container:

18
make.mk
View file

@ -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

View file

@ -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_

View file

@ -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_

View file

@ -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_

View file

@ -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_

View file

@ -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_

View file

@ -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_

View file

@ -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_

View file

@ -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_

View file

@ -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;
}

View file

@ -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_

View file

@ -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.

View file

@ -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/"

View file

@ -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 <emscripten.h>
#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;
}

View file

@ -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;

View file

@ -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_

View file

@ -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_

221
movement/shell.c Normal file
View file

@ -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 <ctype.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#if __EMSCRIPTEN__
#include <emscripten.h>
#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
}

34
movement/shell.h Normal file
View file

@ -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

159
movement/shell_cmd_list.c Normal file
View file

@ -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 <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#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 <PATH>",
.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;
}

38
movement/shell_cmd_list.h Normal file
View file

@ -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 <stdint.h>
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

View file

@ -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));

View file

@ -1,3 +1,27 @@
/*
* MIT License
*
* Copyright (c) 2023 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 <stdlib.h>
#include <string.h>
#include "beats_face.h"

View file

@ -1,6 +1,41 @@
/*
* MIT License
*
* Copyright (c) 2023 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 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 {

View file

@ -0,0 +1,291 @@
/* SPDX-License-Identifier: MIT */
/*
* MIT License
*
* Copyright © 2021-2023 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com>
* Copyright © 2022 David Keck <davidskeck@users.noreply.github.com>
* Copyright © 2022 TheOnePerson <a.nebinger@web.de>
* Copyright © 2023 Jeremy O'Brien <neutral@fastmail.com>
* Copyright © 2023 Mikhail Svarichevsky <3@14.by>
* Copyright © 2023 Wesley Aptekar-Cassels <me@wesleyac.com>
* Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.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
* 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 <stdlib.h>
#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;
}

View file

@ -0,0 +1,60 @@
/* SPDX-License-Identifier: MIT */
/*
* MIT License
*
* Copyright © 2021-2022 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com>
* Copyright © 2022 Alexsander Akers <me@a2.io>
* Copyright © 2022 TheOnePerson <a.nebinger@web.de>
* Copyright © 2023 Alex Utter <ooterness@gmail.com>
* Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.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
* 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_

View file

@ -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 <stdlib.h>
#include <string.h>
#include <math.h>
#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;
}

View file

@ -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_

View file

@ -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?

View file

@ -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 {

View file

@ -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 <stdlib.h>
#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;
}

View file

@ -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_

View file

@ -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:
/*

View file

@ -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;

View file

@ -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) {

View file

@ -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;

View file

@ -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);

View file

@ -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 {

View file

@ -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);

View file

@ -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 {

View file

@ -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 <stdlib.h>
#include <string.h>
#include "world_clock2_face.h"

View file

@ -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

View file

@ -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 youll notice that at first launch the day of week indicators are blank.
* Thats 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;

View file

@ -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

View file

@ -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 {

View file

@ -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);

View file

@ -22,8 +22,6 @@
* SOFTWARE.
*/
//-----------------------------------------------------------------------------
#include <stdlib.h>
#include <string.h>
@ -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,

View file

@ -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

View file

@ -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, youll 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 Earths moon
* MA - Mars
* JU - Jupiter
* SA - Saturn
* UR - Uranus
* NE - Neptune
*
* Once youve 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"

View file

@ -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 batterys 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 {

View file

@ -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);

View file

@ -0,0 +1,267 @@
/*
* MIT License
*
* Copyright (c) 2023 Ekaitz Zarraga <ekaitz@elenq.tech>
*
* 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 <stdlib.h>
#include <string.h>
#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.
}

View file

@ -0,0 +1,87 @@
/*
* MIT License
*
* Copyright (c) 2023 Ekaitz Zarraga <ekaitz@elenq.tech>
*
* 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_

View file

@ -23,27 +23,12 @@
* SOFTWARE.
*/
//-----------------------------------------------------------------------------
#include <stdlib.h>
#include <string.h>
#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

View file

@ -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,

View file

@ -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;

View file

@ -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 <stdlib.h>
@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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 youve 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);

View file

@ -1,7 +1,7 @@
#include <stdlib.h>
#include <string.h>
#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"
/*

View file

@ -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;

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -22,8 +22,6 @@
* SOFTWARE.
*/
//-----------------------------------------------------------------------------
#include <stdlib.h>
#include <string.h>
@ -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,

View file

@ -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)

View file

@ -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;

View file

@ -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 <stdlib.h>
#include <string.h>
#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.
}

View file

@ -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_

View file

@ -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;

View file

@ -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 {

View file

@ -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 <stdlib.h>
#include <string.h>
#include <math.h>

View file

@ -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

View file

@ -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 <stdlib.h>

View file

@ -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 Earths 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 planets location, and
* after a flashing C (for Calculating), you will be presented with the
* planets 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 {

View file

@ -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];

View file

@ -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;

View file

@ -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 {

View file

@ -1,7 +1,11 @@
/* SPDX-License-Identifier: MIT */
/*
* MIT License
*
* Copyright (c) 2022 Joey Castillo
* Copyright © 2021-2022 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com>
* Copyright © 2023 Jeremy O'Brien <neutral@fastmail.com>
* Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.com> (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 <stdlib.h>
#include <string.h>
#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);

View file

@ -1,7 +1,12 @@
/* SPDX-License-Identifier: MIT */
/*
* MIT License
*
* Copyright (c) 2022 Joey Castillo
* Copyright © 2021-2022 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com>
* Copyright © 2022 Alexsander Akers <me@a2.io>
* Copyright © 2023 Alex Utter <ooterness@gmail.com>
* Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.com> (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);

View file

@ -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

View file

@ -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;

View file

@ -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 {

View file

@ -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 <stdlib.h>
#include <string.h>
#include <math.h>

View file

@ -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

View file

@ -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

View file

@ -24,45 +24,12 @@
* SOFTWARE.
*/
//-----------------------------------------------------------------------------
#include <stdlib.h>
#include <string.h>
#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 }

View file

@ -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,

View file

@ -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;

View file

@ -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 <time.h>
#else
#include "saml22j18a.h"
#endif
#include <stdlib.h>
#include <string.h>
#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;
}

View file

@ -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_

View file

@ -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 <stdlib.h>
#include <string.h>
#include <math.h>
#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;
}

View file

@ -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_

View file

@ -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

View file

@ -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 {

View file

@ -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;

View file

@ -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

Some files were not shown because too many files have changed in this diff Show more