mirror of
https://github.com/firewalkwithm3/Sensor-Watch.git
synced 2024-11-22 11:10:29 +08:00
Merge branch 'joeycastillo:main' into main
This commit is contained in:
commit
dde2fd614a
|
@ -1,6 +1,4 @@
|
|||
FROM ubuntu:22.10
|
||||
|
||||
# TODO: install emscripten (https://emscripten.org/docs/getting_started/downloads.html)
|
||||
FROM ubuntu:24.04
|
||||
|
||||
# TODO: Clean this up once buildkit is supported gracefully in devcontainers
|
||||
# https://github.com/microsoft/vscode-remote-release/issues/1409
|
||||
|
@ -27,7 +25,9 @@ RUN apt-get update \
|
|||
# ca certs need to be available for fetching git submodules
|
||||
ca-certificates \
|
||||
# python is used to convert binaries to uf2 files
|
||||
python3 python-is-python3
|
||||
python3 python-is-python3 \
|
||||
# emscripten for building simulator
|
||||
emscripten
|
||||
|
||||
# Download and verify both x86-64 and aarch64 toolchains. This is unfortunate and
|
||||
# slows down the build, but it's a clean-ish option until buildkit can be used.
|
||||
|
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
|||
run: make
|
||||
working-directory: 'movement/make'
|
||||
- name: Upload UF2
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: watch.uf2
|
||||
path: movement/make/build/watch.uf2
|
||||
|
@ -52,7 +52,7 @@ jobs:
|
|||
cp watch.html index.html
|
||||
tar -czf simulator.tar.gz index.html watch.wasm watch.js
|
||||
- name: Upload simulator build
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: simulator.tar.gz
|
||||
path: movement/make/build-sim/simulator.tar.gz
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include "watch.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
const int8_t UTC_OFFSET = 4; // set to your current UTC offset to see correct beats time
|
||||
const uint8_t BEAT_REFRESH_FREQUENCY = 8;
|
||||
|
@ -203,7 +204,6 @@ void set_time_mode_handle_primary_button(void) {
|
|||
|
||||
void set_time_mode_handle_secondary_button(void) {
|
||||
watch_date_time date_time = watch_rtc_get_date_time();
|
||||
const uint8_t days_in_month[12] = {31, 28, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31};
|
||||
|
||||
switch (application_state.page) {
|
||||
case 0: // hour
|
||||
|
@ -224,13 +224,10 @@ void set_time_mode_handle_secondary_button(void) {
|
|||
break;
|
||||
case 5: // day
|
||||
date_time.unit.day = date_time.unit.day + 1;
|
||||
// can't set to the 29th on a leap year. if it's february 29, set to 11:59 on the 28th.
|
||||
// and it should roll over.
|
||||
if (date_time.unit.day > days_in_month[date_time.unit.month - 1]) {
|
||||
date_time.unit.day = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (date_time.unit.day > days_in_month(date_time.unit.month, date_time.unit.year + WATCH_RTC_REFERENCE_YEAR))
|
||||
date_time.unit.day = 1;
|
||||
watch_rtc_set_date_time(date_time);
|
||||
}
|
||||
|
||||
|
|
14
make.mk
14
make.mk
|
@ -215,6 +215,20 @@ SRCS += \
|
|||
|
||||
endif
|
||||
|
||||
ifeq ($(LED), BLUE)
|
||||
CFLAGS += -DWATCH_IS_BLUE_BOARD
|
||||
endif
|
||||
|
||||
ifndef COLOR
|
||||
$(error Set the COLOR variable to RED, BLUE, or GREEN depending on what board you have.)
|
||||
endif
|
||||
|
||||
COLOR_VALID := $(filter $(COLOR),RED BLUE GREEN)
|
||||
|
||||
ifeq ($(COLOR_VALID),)
|
||||
$(error COLOR must be RED, BLUE, or GREEN)
|
||||
endif
|
||||
|
||||
ifeq ($(COLOR), BLUE)
|
||||
CFLAGS += -DWATCH_IS_BLUE_BOARD
|
||||
endif
|
||||
|
|
|
@ -120,13 +120,29 @@ bool filesystem_init(void) {
|
|||
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;
|
||||
err = lfs_mount(&lfs, &cfg);
|
||||
printf("Filesystem mounted with %ld bytes free.\r\n", filesystem_get_free_space());
|
||||
}
|
||||
|
||||
return err == LFS_ERR_OK;
|
||||
}
|
||||
|
||||
int _filesystem_format(void);
|
||||
int _filesystem_format(void) {
|
||||
int err = lfs_unmount(&lfs);
|
||||
if (err < 0) {
|
||||
printf("Couldn't unmount - continuing to format, but you should reboot afterwards!\r\n");
|
||||
}
|
||||
|
||||
err = lfs_format(&lfs, &cfg);
|
||||
if (err < 0) return err;
|
||||
|
||||
err = lfs_mount(&lfs, &cfg);
|
||||
if (err < 0) return err;
|
||||
printf("Filesystem re-mounted with %ld bytes free.\r\n", filesystem_get_free_space());
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool filesystem_file_exists(char *filename) {
|
||||
info.type = 0;
|
||||
lfs_stat(&lfs, filename, &info);
|
||||
|
@ -251,6 +267,16 @@ int filesystem_cmd_rm(int argc, char *argv[]) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int filesystem_cmd_format(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
if(strcmp(argv[1], "YES") == 0) {
|
||||
return _filesystem_format();
|
||||
}
|
||||
printf("usage: format YES\r\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int filesystem_cmd_echo(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
|
||||
|
@ -279,4 +305,3 @@ int filesystem_cmd_echo(int argc, char *argv[]) {
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -100,6 +100,7 @@ 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_format(int argc, char *argv[]);
|
||||
int filesystem_cmd_echo(int argc, char *argv[]);
|
||||
|
||||
#endif // FILESYSTEM_H_
|
||||
|
|
3704
movement/lib/smallchesslib/smallchesslib.h
Normal file
3704
movement/lib/smallchesslib/smallchesslib.h
Normal file
File diff suppressed because it is too large
Load diff
|
@ -24,6 +24,7 @@ INCLUDES += \
|
|||
-I../lib/vsop87/ \
|
||||
-I../lib/astrolib/ \
|
||||
-I../lib/morsecalc/ \
|
||||
-I../lib/smallchesslib/ \
|
||||
|
||||
# If you add any other source files you wish to compile, add them after ../app.c
|
||||
# Note that you will need to add a backslash at the end of any line you wish to continue, i.e.
|
||||
|
@ -52,6 +53,7 @@ SRCS += \
|
|||
../shell.c \
|
||||
../shell_cmd_list.c \
|
||||
../watch_faces/clock/simple_clock_face.c \
|
||||
../watch_faces/clock/close_enough_clock_face.c \
|
||||
../watch_faces/clock/clock_face.c \
|
||||
../watch_faces/clock/world_clock_face.c \
|
||||
../watch_faces/clock/beats_face.c \
|
||||
|
@ -118,6 +120,7 @@ SRCS += \
|
|||
../watch_faces/complication/toss_up_face.c \
|
||||
../watch_faces/complication/geomancy_face.c \
|
||||
../watch_faces/clock/simple_clock_bin_led_face.c \
|
||||
../watch_faces/complication/menstrual_cycle_face.c \
|
||||
../watch_faces/complication/flashlight_face.c \
|
||||
../watch_faces/clock/decimal_time_face.c \
|
||||
../watch_faces/clock/wyoscan_face.c \
|
||||
|
@ -128,7 +131,24 @@ SRCS += \
|
|||
../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/sensor/minmax_face.c \
|
||||
../watch_faces/complication/kitchen_conversions_face.c \
|
||||
../watch_faces/complication/butterfly_game_face.c \
|
||||
../watch_faces/complication/wareki_face.c \
|
||||
../watch_faces/complication/wordle_face.c \
|
||||
../watch_faces/complication/endless_runner_face.c \
|
||||
../watch_faces/complication/periodic_face.c \
|
||||
../watch_faces/complication/deadline_face.c \
|
||||
../watch_faces/complication/higher_lower_game_face.c \
|
||||
../watch_faces/clock/french_revolutionary_face.c \
|
||||
../watch_faces/clock/minimal_clock_face.c \
|
||||
../watch_faces/complication/simon_face.c \
|
||||
../watch_faces/complication/simple_calculator_face.c \
|
||||
../watch_faces/sensor/alarm_thermometer_face.c \
|
||||
../watch_faces/demo/beeps_face.c \
|
||||
../watch_faces/sensor/accel_interrupt_count_face.c \
|
||||
../watch_faces/complication/metronome_face.c \
|
||||
../watch_faces/complication/smallchess_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.
|
||||
|
|
|
@ -201,7 +201,7 @@ static void _movement_handle_scheduled_tasks(void) {
|
|||
|
||||
for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
|
||||
if (scheduled_tasks[i].reg) {
|
||||
if (scheduled_tasks[i].reg == date_time.reg) {
|
||||
if (scheduled_tasks[i].reg <= date_time.reg) {
|
||||
scheduled_tasks[i].reg = 0;
|
||||
movement_event_t background_event = { EVENT_BACKGROUND_TASK, 0 };
|
||||
watch_faces[i].loop(background_event, &movement_state.settings, watch_face_contexts[i]);
|
||||
|
@ -239,14 +239,24 @@ void movement_request_tick_frequency(uint8_t freq) {
|
|||
}
|
||||
|
||||
void movement_illuminate_led(void) {
|
||||
if (movement_state.settings.bit.led_duration) {
|
||||
if (movement_state.settings.bit.led_duration != 0b111) {
|
||||
watch_set_led_color(movement_state.settings.bit.led_red_color ? (0xF | movement_state.settings.bit.led_red_color << 4) : 0,
|
||||
movement_state.settings.bit.led_green_color ? (0xF | movement_state.settings.bit.led_green_color << 4) : 0);
|
||||
if (movement_state.settings.bit.led_duration == 0) {
|
||||
movement_state.light_ticks = 1;
|
||||
} else {
|
||||
movement_state.light_ticks = (movement_state.settings.bit.led_duration * 2 - 1) * 128;
|
||||
}
|
||||
_movement_enable_fast_tick_if_needed();
|
||||
}
|
||||
}
|
||||
|
||||
static void _movement_led_off(void) {
|
||||
watch_set_led_off();
|
||||
movement_state.light_ticks = -1;
|
||||
_movement_disable_fast_tick_if_possible();
|
||||
}
|
||||
|
||||
bool movement_default_loop_handler(movement_event_t event, movement_settings_t *settings) {
|
||||
(void)settings;
|
||||
|
||||
|
@ -257,6 +267,11 @@ bool movement_default_loop_handler(movement_event_t event, movement_settings_t *
|
|||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
movement_illuminate_led();
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
if (movement_state.settings.bit.led_duration == 0) {
|
||||
_movement_led_off();
|
||||
}
|
||||
break;
|
||||
case EVENT_MODE_LONG_PRESS:
|
||||
if (MOVEMENT_SECONDARY_FACE_INDEX && movement_state.current_face_idx == 0) {
|
||||
movement_move_to_face(MOVEMENT_SECONDARY_FACE_INDEX);
|
||||
|
@ -328,6 +343,14 @@ static void end_buzzing_and_disable_buzzer(void) {
|
|||
watch_disable_buzzer();
|
||||
}
|
||||
|
||||
static void set_initial_clock_mode(void) {
|
||||
#ifdef CLOCK_FACE_24H_ONLY
|
||||
movement_state.settings.bit.clock_mode_24h = true;
|
||||
#else
|
||||
movement_state.settings.bit.clock_mode_24h = MOVEMENT_DEFAULT_24H_MODE;
|
||||
#endif
|
||||
}
|
||||
|
||||
void movement_play_signal(void) {
|
||||
void *maybe_disable_buzzer = end_buzzing_and_disable_buzzer;
|
||||
if (watch_is_buzzer_or_led_enabled()) {
|
||||
|
@ -376,14 +399,14 @@ void app_init(void) {
|
|||
#endif
|
||||
|
||||
memset(&movement_state, 0, sizeof(movement_state));
|
||||
|
||||
movement_state.settings.bit.clock_mode_24h = MOVEMENT_DEFAULT_24H_MODE;
|
||||
set_initial_clock_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 = 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;
|
||||
|
@ -503,9 +526,7 @@ bool app_loop(void) {
|
|||
if (watch_get_pin_level(BTN_LIGHT)) {
|
||||
movement_state.light_ticks = 1;
|
||||
} else {
|
||||
watch_set_led_off();
|
||||
movement_state.light_ticks = -1;
|
||||
_movement_disable_fast_tick_if_possible();
|
||||
_movement_led_off();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -543,6 +564,17 @@ bool app_loop(void) {
|
|||
event.subsecond = movement_state.subsecond;
|
||||
// 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]);
|
||||
|
||||
// Keep light on if user is still interacting with the watch.
|
||||
if (movement_state.light_ticks > 0) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
case EVENT_MODE_BUTTON_DOWN:
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
movement_illuminate_led();
|
||||
}
|
||||
}
|
||||
|
||||
event.event_type = EVENT_NONE;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ typedef union {
|
|||
uint8_t to_interval : 2; // an inactivity interval for asking the active face to resign.
|
||||
bool to_always : 1; // if true, always time out from the active face to face 0. otherwise only faces that time out will resign (the default).
|
||||
uint8_t le_interval : 3; // 0 to disable low energy mode, or an inactivity interval for going into low energy mode.
|
||||
uint8_t led_duration : 2; // how many seconds to shine the LED for (x2), or 0 to disable it.
|
||||
uint8_t led_duration : 3; // how many seconds to shine the LED for (x2), 0 to shine only while the button is depressed, or all bits set to disable the LED altogether.
|
||||
uint8_t led_red_color : 4; // for general purpose illumination, the red LED value (0-15)
|
||||
uint8_t led_green_color : 4; // for general purpose illumination, the green LED value (0-15)
|
||||
uint8_t time_zone : 6; // an integer representing an index in the time zone table.
|
||||
|
@ -60,9 +60,10 @@ typedef union {
|
|||
// time-oriented complication like a sunrise/sunset timer, and a simple locale preference could tell an
|
||||
// altimeter to display feet or meters as easily as it tells a thermometer to display degrees in F or C.
|
||||
bool clock_mode_24h : 1; // indicates whether clock should use 12 or 24 hour mode.
|
||||
bool clock_24h_leading_zero : 1; // indicates whether clock should leading zero to indicate 24 hour mode.
|
||||
bool use_imperial_units : 1; // indicates whether to use metric units (the default) or imperial.
|
||||
bool alarm_enabled : 1; // indicates whether there is at least one alarm enabled.
|
||||
uint8_t reserved : 6; // room for more preferences if needed.
|
||||
uint8_t reserved : 5; // room for more preferences if needed.
|
||||
} bit;
|
||||
uint32_t reg;
|
||||
} movement_settings_t;
|
||||
|
|
|
@ -67,21 +67,139 @@ int8_t signal_tune[] = {
|
|||
};
|
||||
#endif // SIGNAL_TUNE_MARIO_THEME
|
||||
|
||||
#ifdef SIGNAL_TUNE_MGS_CODEC
|
||||
int8_t signal_tune[] = {
|
||||
BUZZER_NOTE_G5SHARP_A5FLAT, 1,
|
||||
BUZZER_NOTE_C6, 1,
|
||||
BUZZER_NOTE_G5SHARP_A5FLAT, 1,
|
||||
BUZZER_NOTE_C6, 1,
|
||||
BUZZER_NOTE_G5SHARP_A5FLAT, 1,
|
||||
BUZZER_NOTE_C6, 1,
|
||||
BUZZER_NOTE_G5SHARP_A5FLAT, 1,
|
||||
BUZZER_NOTE_C6, 1,
|
||||
BUZZER_NOTE_G5SHARP_A5FLAT, 1,
|
||||
BUZZER_NOTE_C6, 1,
|
||||
BUZZER_NOTE_REST, 6,
|
||||
BUZZER_NOTE_G5SHARP_A5FLAT, 1,
|
||||
BUZZER_NOTE_C6, 1,
|
||||
BUZZER_NOTE_G5SHARP_A5FLAT, 1,
|
||||
BUZZER_NOTE_C6, 1,
|
||||
BUZZER_NOTE_G5SHARP_A5FLAT, 1,
|
||||
BUZZER_NOTE_C6, 1,
|
||||
BUZZER_NOTE_G5SHARP_A5FLAT, 1,
|
||||
BUZZER_NOTE_C6, 1,
|
||||
BUZZER_NOTE_G5SHARP_A5FLAT, 1,
|
||||
BUZZER_NOTE_C6, 1,
|
||||
0
|
||||
};
|
||||
#endif // SIGNAL_TUNE_MGS_CODEC
|
||||
|
||||
#ifdef SIGNAL_TUNE_KIM_POSSIBLE
|
||||
int8_t signal_tune[] = {
|
||||
BUZZER_NOTE_G7, 6,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_G4, 3,
|
||||
BUZZER_NOTE_G4, 2,
|
||||
BUZZER_NOTE_REST, 5,
|
||||
BUZZER_NOTE_G7, 6,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_G4, 3,
|
||||
BUZZER_NOTE_G4, 2,
|
||||
BUZZER_NOTE_REST, 5,
|
||||
BUZZER_NOTE_A7SHARP_B7FLAT, 6,
|
||||
BUZZER_NOTE_REST, 2,
|
||||
BUZZER_NOTE_G7, 6,
|
||||
BUZZER_NOTE_G4, 2,
|
||||
0
|
||||
};
|
||||
#endif // SIGNAL_TUNE_KIM_POSSIBLE
|
||||
|
||||
#ifdef SIGNAL_TUNE_POWER_RANGERS
|
||||
int8_t signal_tune[] = {
|
||||
BUZZER_NOTE_D8, 6,
|
||||
BUZZER_NOTE_REST, 8,
|
||||
BUZZER_NOTE_D8, 6,
|
||||
BUZZER_NOTE_REST, 8,
|
||||
BUZZER_NOTE_C8, 6,
|
||||
BUZZER_NOTE_REST, 2,
|
||||
BUZZER_NOTE_D8, 6,
|
||||
BUZZER_NOTE_REST, 8,
|
||||
BUZZER_NOTE_F8, 6,
|
||||
BUZZER_NOTE_REST, 8,
|
||||
BUZZER_NOTE_D8, 6,
|
||||
0
|
||||
};
|
||||
#endif // SIGNAL_TUNE_POWER_RANGERS
|
||||
|
||||
#ifdef SIGNAL_TUNE_LAYLA
|
||||
int8_t signal_tune[] = {
|
||||
BUZZER_NOTE_A6, 5,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_C7, 5,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_D7, 5,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_F7, 5,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_D7, 5,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_C7, 5,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_D7, 20,
|
||||
0
|
||||
};
|
||||
#endif // SIGNAL_TUNE_LAYLA
|
||||
|
||||
#ifdef SIGNAL_TUNE_HARRY_POTTER_SHORT
|
||||
int8_t signal_tune[] = {
|
||||
BUZZER_NOTE_B5, 12,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_E6, 12,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_G6, 6,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_F6SHARP_G6FLAT, 6,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_E6, 16,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_B6, 8,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_A6, 24,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_F6SHARP_G6FLAT, 24,
|
||||
0
|
||||
};
|
||||
#endif // SIGNAL_TUNE_HARRY_POTTER_SHORT
|
||||
|
||||
#ifdef SIGNAL_TUNE_HARRY_POTTER_LONG
|
||||
int8_t signal_tune[] = {
|
||||
BUZZER_NOTE_B5, 12,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_E6, 12,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_G6, 6,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_F6SHARP_G6FLAT, 6,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_E6, 16,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_B6, 8,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_A6, 24,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_F6SHARP_G6FLAT, 24,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
|
||||
BUZZER_NOTE_E6, 12,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_G6, 6,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_F6SHARP_G6FLAT, 6,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_D6SHARP_E6FLAT, 16,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_F6, 8,
|
||||
BUZZER_NOTE_REST, 1,
|
||||
BUZZER_NOTE_B5, 24,
|
||||
|
||||
0
|
||||
};
|
||||
#endif // SIGNAL_TUNE_HARRY_POTTER_LONG
|
||||
|
||||
#endif // MOVEMENT_CUSTOM_SIGNAL_TUNES_H_
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#define MOVEMENT_FACES_H_
|
||||
|
||||
#include "simple_clock_face.h"
|
||||
#include "close_enough_clock_face.h"
|
||||
#include "clock_face.h"
|
||||
#include "world_clock_face.h"
|
||||
#include "preferences_face.h"
|
||||
|
@ -93,6 +94,7 @@
|
|||
#include "geomancy_face.h"
|
||||
#include "dual_timer_face.h"
|
||||
#include "simple_clock_bin_led_face.h"
|
||||
#include "menstrual_cycle_face.h"
|
||||
#include "flashlight_face.h"
|
||||
#include "decimal_time_face.h"
|
||||
#include "wyoscan_face.h"
|
||||
|
@ -103,7 +105,24 @@
|
|||
#include "couch_to_5k_face.h"
|
||||
#include "minute_repeater_decimal_face.h"
|
||||
#include "tuning_tones_face.h"
|
||||
#include "minmax_face.h"
|
||||
#include "kitchen_conversions_face.h"
|
||||
#include "butterfly_game_face.h"
|
||||
#include "wareki_face.h"
|
||||
#include "wordle_face.h"
|
||||
#include "endless_runner_face.h"
|
||||
#include "periodic_face.h"
|
||||
#include "deadline_face.h"
|
||||
#include "higher_lower_game_face.h"
|
||||
#include "french_revolutionary_face.h"
|
||||
#include "minimal_clock_face.h"
|
||||
#include "simon_face.h"
|
||||
#include "simple_calculator_face.h"
|
||||
#include "alarm_thermometer_face.h"
|
||||
#include "beeps_face.h"
|
||||
#include "accel_interrupt_count_face.h"
|
||||
#include "metronome_face.h"
|
||||
#include "smallchess_face.h"
|
||||
// New includes go above this line.
|
||||
|
||||
#endif // MOVEMENT_FACES_H_
|
||||
|
|
|
@ -85,6 +85,13 @@ shell_command_t g_shell_commands[] = {
|
|||
.max_args = 1,
|
||||
.cb = filesystem_cmd_rm,
|
||||
},
|
||||
{
|
||||
.name = "format",
|
||||
.help = "usage: format YES",
|
||||
.min_args = 1,
|
||||
.max_args = 1,
|
||||
.cb = filesystem_cmd_format,
|
||||
},
|
||||
{
|
||||
.name = "echo",
|
||||
.help = "usage: echo TEXT {>,>>} FILE",
|
||||
|
@ -156,4 +163,3 @@ static int stress_cmd(int argc, char *argv[]) {
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -42,10 +42,6 @@
|
|||
#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;
|
||||
|
@ -57,8 +53,15 @@ typedef struct {
|
|||
} clock_state_t;
|
||||
|
||||
static bool clock_is_in_24h_mode(movement_settings_t *settings) {
|
||||
if (CLOCK_FACE_24H_ONLY) { return true; }
|
||||
#ifdef CLOCK_FACE_24H_ONLY
|
||||
return true;
|
||||
#else
|
||||
return settings->bit.clock_mode_24h;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool clock_should_set_leading_zero(movement_settings_t *settings) {
|
||||
return clock_is_in_24h_mode(settings) && settings->bit.clock_24h_leading_zero;
|
||||
}
|
||||
|
||||
static void clock_indicate(WatchIndicatorSegment indicator, bool on) {
|
||||
|
@ -70,11 +73,11 @@ static void clock_indicate(WatchIndicatorSegment indicator, bool on) {
|
|||
}
|
||||
|
||||
static void clock_indicate_alarm(movement_settings_t *settings) {
|
||||
clock_indicate(WATCH_INDICATOR_BELL, settings->bit.alarm_enabled);
|
||||
clock_indicate(WATCH_INDICATOR_SIGNAL, settings->bit.alarm_enabled);
|
||||
}
|
||||
|
||||
static void clock_indicate_time_signal(clock_state_t *clock) {
|
||||
clock_indicate(WATCH_INDICATOR_SIGNAL, clock->time_signal_enabled);
|
||||
clock_indicate(WATCH_INDICATOR_BELL, clock->time_signal_enabled);
|
||||
}
|
||||
|
||||
static void clock_indicate_24h(movement_settings_t *settings) {
|
||||
|
@ -125,13 +128,13 @@ static void clock_toggle_time_signal(clock_state_t *clock) {
|
|||
clock_indicate_time_signal(clock);
|
||||
}
|
||||
|
||||
static void clock_display_all(watch_date_time date_time) {
|
||||
static void clock_display_all(watch_date_time date_time, bool leading_zero) {
|
||||
char buf[10 + 1];
|
||||
|
||||
snprintf(
|
||||
buf,
|
||||
sizeof(buf),
|
||||
"%s%2d%2d%02d%02d",
|
||||
leading_zero? "%s%02d%02d%02d%02d" : "%s%2d%2d%02d%02d",
|
||||
watch_utility_get_weekday(date_time),
|
||||
date_time.unit.day,
|
||||
date_time.unit.hour,
|
||||
|
@ -181,7 +184,7 @@ static void clock_display_clock(movement_settings_t *settings, clock_state_t *cl
|
|||
clock_indicate_pm(settings, current);
|
||||
current = clock_24h_to_12h(current);
|
||||
}
|
||||
clock_display_all(current);
|
||||
clock_display_all(current, clock_should_set_leading_zero(settings));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
233
movement/watch_faces/clock/close_enough_clock_face.c
Normal file
233
movement/watch_faces/clock/close_enough_clock_face.c
Normal file
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Ruben Nic
|
||||
*
|
||||
* 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 "close_enough_clock_face.h"
|
||||
#include "watch.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
const char *words[12] = {
|
||||
" ",
|
||||
" 5",
|
||||
"10",
|
||||
"15",
|
||||
"20",
|
||||
"25",
|
||||
"30",
|
||||
"35",
|
||||
"40",
|
||||
"45",
|
||||
"50",
|
||||
"55",
|
||||
};
|
||||
|
||||
static const char *past_word = " P";
|
||||
static const char *to_word = " 2";
|
||||
static const char *oclock_word = "OC";
|
||||
|
||||
// sets when in the five minute period we switch
|
||||
// from "X past HH" to "X to HH+1"
|
||||
static const int hour_switch_index = 8;
|
||||
|
||||
static void _update_alarm_indicator(bool settings_alarm_enabled, close_enough_clock_state_t *state) {
|
||||
state->alarm_enabled = settings_alarm_enabled;
|
||||
if (state->alarm_enabled) {
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
};
|
||||
}
|
||||
|
||||
void close_enough_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(close_enough_clock_state_t));
|
||||
}
|
||||
}
|
||||
|
||||
void close_enough_clock_face_activate(movement_settings_t *settings, void *context) {
|
||||
close_enough_clock_state_t *state = (close_enough_clock_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);
|
||||
}
|
||||
|
||||
// show alarm indicator if there is an active alarm
|
||||
_update_alarm_indicator(settings->bit.alarm_enabled, state);
|
||||
|
||||
// this ensures that none of the five_minute_periods will match, so we always rerender when the face activates
|
||||
state->prev_five_minute_period = -1;
|
||||
state->prev_min_checked = -1;
|
||||
}
|
||||
|
||||
bool close_enough_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
close_enough_clock_state_t *state = (close_enough_clock_state_t *)context;
|
||||
|
||||
char buf[11];
|
||||
watch_date_time date_time;
|
||||
bool show_next_hour = false;
|
||||
int prev_five_minute_period;
|
||||
int prev_min_checked;
|
||||
int close_enough_hour;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
case EVENT_TICK:
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
date_time = watch_rtc_get_date_time();
|
||||
prev_five_minute_period = state->prev_five_minute_period;
|
||||
prev_min_checked = state->prev_min_checked;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// same minute, skip update
|
||||
if (date_time.unit.minute == prev_min_checked) {
|
||||
break;
|
||||
} else {
|
||||
state->prev_min_checked = date_time.unit.minute;
|
||||
}
|
||||
|
||||
int five_minute_period = (date_time.unit.minute / 5) % 12;
|
||||
|
||||
// If we are 60% to the next 5 interval, move up to the next period
|
||||
if (fmodf(date_time.unit.minute / 5.0f, 1.0f) > 0.5f) {
|
||||
// If we are on the last 5 interval and moving to the next period we need to display the next hour because we are wrapping around
|
||||
if (five_minute_period == 11) {
|
||||
show_next_hour = true;
|
||||
}
|
||||
|
||||
five_minute_period = (five_minute_period + 1) % 12;
|
||||
}
|
||||
|
||||
// same five_minute_period, skip update
|
||||
if (five_minute_period == prev_five_minute_period) {
|
||||
break;
|
||||
}
|
||||
|
||||
// we don't want to modify date_time.unit.hour just in case other watch faces use it
|
||||
close_enough_hour = date_time.unit.hour;
|
||||
|
||||
// move from "MM(mins) P HH" to "MM(mins) 2 HH+1"
|
||||
if (five_minute_period >= hour_switch_index || show_next_hour) {
|
||||
close_enough_hour = (close_enough_hour + 1) % 24;
|
||||
}
|
||||
|
||||
if (!settings->bit.clock_mode_24h) {
|
||||
// if we are in 12 hour mode, do some cleanup.
|
||||
if (close_enough_hour < 12) {
|
||||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
} else {
|
||||
watch_set_indicator(WATCH_INDICATOR_PM);
|
||||
}
|
||||
|
||||
close_enough_hour %= 12;
|
||||
if (close_enough_hour == 0) {
|
||||
close_enough_hour = 12;
|
||||
}
|
||||
|
||||
date_time.unit.hour %= 12;
|
||||
if (date_time.unit.hour == 0) {
|
||||
date_time.unit.hour = 12;
|
||||
}
|
||||
}
|
||||
|
||||
char first_word[3];
|
||||
char second_word[3];
|
||||
char third_word[3];
|
||||
if (five_minute_period == 0) { // "HH OC",
|
||||
sprintf(first_word, "%2d", close_enough_hour);
|
||||
strncpy(second_word, words[five_minute_period], 3);
|
||||
strncpy(third_word, oclock_word, 3);
|
||||
} else {
|
||||
int words_length = sizeof(words) / sizeof(words[0]);
|
||||
|
||||
strncpy(
|
||||
first_word,
|
||||
five_minute_period >= hour_switch_index ?
|
||||
words[words_length - five_minute_period] :
|
||||
words[five_minute_period],
|
||||
3
|
||||
);
|
||||
strncpy(
|
||||
second_word,
|
||||
five_minute_period >= hour_switch_index ?
|
||||
to_word : past_word,
|
||||
3
|
||||
);
|
||||
sprintf(third_word, "%2d", close_enough_hour);
|
||||
}
|
||||
|
||||
sprintf(
|
||||
buf,
|
||||
"%s%2d%s%s%s",
|
||||
watch_utility_get_weekday(date_time),
|
||||
date_time.unit.day,
|
||||
first_word,
|
||||
second_word,
|
||||
third_word
|
||||
);
|
||||
|
||||
watch_display_string(buf, 0);
|
||||
state->prev_five_minute_period = five_minute_period;
|
||||
|
||||
// handle alarm indicator
|
||||
if (state->alarm_enabled != settings->bit.alarm_enabled) {
|
||||
_update_alarm_indicator(settings->bit.alarm_enabled, state);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void close_enough_clock_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
62
movement/watch_faces/clock/close_enough_clock_face.h
Normal file
62
movement/watch_faces/clock/close_enough_clock_face.h
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Ruben Nic
|
||||
*
|
||||
* 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 CLOSE_ENOUGH_CLOCK_FACE_H_
|
||||
#define CLOSE_ENOUGH_CLOCK_FACE_H_
|
||||
|
||||
/*
|
||||
* CLOSE ENOUGH CLOCK FACE
|
||||
*
|
||||
* Displays the current time; but only in periods of 5.
|
||||
* Just in the in the formats of:
|
||||
* - "10 past 5"
|
||||
* - "15 to 7"
|
||||
* - "6 o'clock"
|
||||
*
|
||||
*/
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
typedef struct {
|
||||
int prev_five_minute_period;
|
||||
int prev_min_checked;
|
||||
uint8_t last_battery_check;
|
||||
bool battery_low;
|
||||
bool alarm_enabled;
|
||||
} close_enough_clock_state_t;
|
||||
|
||||
void close_enough_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void close_enough_clock_face_activate(movement_settings_t *settings, void *context);
|
||||
bool close_enough_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void close_enough_clock_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define close_enough_clock_face ((const watch_face_t){ \
|
||||
close_enough_clock_face_setup, \
|
||||
close_enough_clock_face_activate, \
|
||||
close_enough_clock_face_loop, \
|
||||
close_enough_clock_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // CLOSE_ENOUGH_CLOCK_FACE_H_
|
245
movement/watch_faces/clock/french_revolutionary_face.c
Normal file
245
movement/watch_faces/clock/french_revolutionary_face.c
Normal file
|
@ -0,0 +1,245 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 CarpeNoctem
|
||||
*
|
||||
* 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 "french_revolutionary_face.h"
|
||||
|
||||
void french_revolutionary_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(french_revolutionary_state_t));
|
||||
memset(*context_ptr, 0, sizeof(french_revolutionary_state_t));
|
||||
// Do any one-time tasks in here; the inside of this conditional happens only at boot.
|
||||
french_revolutionary_state_t *state = (french_revolutionary_state_t *)*context_ptr;
|
||||
state->use_am_pm = false;
|
||||
state->show_seconds = true;
|
||||
state->display_type = 0;
|
||||
state->colon_set_after_splash = false;
|
||||
}
|
||||
// Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.
|
||||
}
|
||||
|
||||
void french_revolutionary_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
french_revolutionary_state_t *state = (french_revolutionary_state_t *)context;
|
||||
|
||||
// Handle any tasks related to your watch face coming on screen.
|
||||
state->colon_set_after_splash = false;
|
||||
}
|
||||
|
||||
bool french_revolutionary_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
french_revolutionary_state_t *state = (french_revolutionary_state_t *)context;
|
||||
|
||||
char buf[11];
|
||||
watch_date_time date_time;
|
||||
fr_decimal_time decimal_time;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
// Initial UI - Show a quick "splash screen"
|
||||
watch_clear_display();
|
||||
watch_display_string("FR dECimL", 0);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
|
||||
date_time = watch_rtc_get_date_time();
|
||||
|
||||
decimal_time = get_decimal_time(&date_time);
|
||||
|
||||
set_display_buffer(buf, state, &decimal_time, &date_time);
|
||||
|
||||
// If we're in low-energy mode, don't write out the seconds. Also start the LE tick animation if it's not already going.
|
||||
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
|
||||
buf[8] = ' ';
|
||||
buf[9] = ' ';
|
||||
if (!watch_tick_animation_is_running()) { watch_start_tick_animation(500); }
|
||||
}
|
||||
|
||||
// Update the display with our decimal time
|
||||
watch_display_string(buf, 0);
|
||||
|
||||
// Oh, and a one-off to set the colon after the "splash screen"
|
||||
if (!state->colon_set_after_splash) {
|
||||
watch_set_colon();
|
||||
state->colon_set_after_splash = true;
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
state->display_type += 1 ; // cycle through the display types
|
||||
if (state->display_type > 2) { state->display_type = 0; } // but return to 0 after 2
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
// I originally had chiming on the decimal-hour enabled, and this would enable/disable that chime, just like on
|
||||
// the simple clock and decimal time faces. But because decimal seconds don't always line up with normal seconds,
|
||||
// I assume the (decimal-)hourly chime could sometimes be missed. Additionally, I need this button for other purposes,
|
||||
// now that I added seconds on/off toggle and upper normal-time with the ability to toggle that between 12/24hr format.
|
||||
state->show_seconds = !state->show_seconds;
|
||||
if (!state->show_seconds) { watch_display_string(" ", 8); }
|
||||
else { watch_display_string("--", 8); }
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
// In case anyone really wants that upper time in 12-hour format. I thought about using the global setting (settings->bit.clock_mode_24h)
|
||||
// for this preference, but thought someone who prefers 12-hour format normally, might prefer 24hr when compared to a 10hr decimal day,
|
||||
// so this is separate for now.
|
||||
state->use_am_pm = !state->use_am_pm;
|
||||
if (state->use_am_pm) {
|
||||
watch_clear_indicator(WATCH_INDICATOR_24H);
|
||||
date_time = watch_rtc_get_date_time();
|
||||
if (date_time.unit.hour < 12) { watch_clear_indicator(WATCH_INDICATOR_PM); }
|
||||
else { watch_set_indicator(WATCH_INDICATOR_PM); }
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
}
|
||||
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 french_revolutionary_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
|
||||
// handle any cleanup before your watch face goes off-screen.
|
||||
}
|
||||
|
||||
// Calculate decimal time from normal (24hr) time
|
||||
fr_decimal_time get_decimal_time(watch_date_time *date_time) {
|
||||
uint32_t current_24hr_secs, current_decimal_seconds;
|
||||
fr_decimal_time decimal_time;
|
||||
// Current 24-hr time in seconds (There are 86400 of these in a day.)
|
||||
current_24hr_secs = date_time->unit.hour * 3600 + date_time->unit.minute * 60 + date_time->unit.second;
|
||||
|
||||
// Current Decimal Time in seconds. There are 100000 seconds in a 10-hr decimal-time day.
|
||||
// current_decimal_seconds = current_24hr_seconds * 100000 / 86400, or = current_24_seconds * 1000 / 864;
|
||||
// By chopping the extra zeros off the end, we can use uint32 instead of uint64.
|
||||
current_decimal_seconds = current_24hr_secs * 1000 / 864;
|
||||
|
||||
decimal_time.hour = current_decimal_seconds / 10000;
|
||||
// Remove the hours from total seconds and keep the remainder for below.
|
||||
current_decimal_seconds = current_decimal_seconds - decimal_time.hour * 10000;
|
||||
|
||||
decimal_time.minute = current_decimal_seconds / 100;
|
||||
// Remove the minutes from total seconds and keep the remaining seconds
|
||||
// Note: I think I used an extra seconds variable here because sprintf or movement weren't liking a uint32...
|
||||
decimal_time.second = current_decimal_seconds - decimal_time.minute * 100;
|
||||
return decimal_time;
|
||||
}
|
||||
|
||||
// Fills in the display buffer, depending on the currently-selected display option (and sub-options):
|
||||
// - Decimal-time only
|
||||
// - Decimal-time with date in top-right
|
||||
// - Decimal-time with normal time in the top (minutes first, then hours, due to display limitations)
|
||||
// TODO: There is some power-saving stuff that simple clock does here around not redrawing characters that haven't changed, but we're not doing that here.
|
||||
// I'll try to add that optimization could be added in a future commit.
|
||||
void set_display_buffer(char *buf, french_revolutionary_state_t *state, fr_decimal_time *decimal_time, watch_date_time *date_time) {
|
||||
switch (state->display_type) {
|
||||
// Decimal time only
|
||||
case 0:
|
||||
// Originally I had the day slot set to "FR" (French Revolutionary time), but my brain kept thinking "Friday" whenever I saw it,
|
||||
// so I changed it to dT (Decimal Time) to avoid that confusion. Apologies to anyone who has the other decimal_time face and this one
|
||||
// installed concurrently. Maybe the splash screen will help a little.
|
||||
sprintf( buf, "dT %2d%02d%02d", decimal_time->hour, decimal_time->minute, decimal_time->second );
|
||||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
watch_clear_indicator(WATCH_INDICATOR_24H);
|
||||
break;
|
||||
// Decimal time and date
|
||||
case 1:
|
||||
sprintf( buf, "dT%2d%2d%02d%02d", date_time->unit.day, decimal_time->hour, decimal_time->minute, decimal_time->second );
|
||||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
watch_clear_indicator(WATCH_INDICATOR_24H);
|
||||
break;
|
||||
// Decimal time on bottom, normal time above
|
||||
case 2:
|
||||
if (state->use_am_pm) {
|
||||
// if we are in 12 hour mode, do some cleanup.
|
||||
watch_clear_indicator(WATCH_INDICATOR_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;
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
}
|
||||
// Note, the date digits don't display a leading zero well, so we don't use it.
|
||||
sprintf( buf, "%02d%2d%2d%02d%02d", date_time->unit.minute, date_time->unit.hour, decimal_time->hour, decimal_time->minute, decimal_time->second );
|
||||
|
||||
// Make the second character of the Day area more readable
|
||||
buf[1] = fix_character_one(buf[1]);
|
||||
break;
|
||||
}
|
||||
// Finally, if show_seconds is disabled, trim those off.
|
||||
if (!state->show_seconds) {
|
||||
buf[8] = ' ';
|
||||
buf[9] = ' ';
|
||||
}
|
||||
}
|
||||
|
||||
// Sadly, the second character of the Day field cannot show all numbers, so we make some replacements.
|
||||
// See https://www.sensorwatch.net/docs/wig/display/#limitations-of-the-weekday-digits
|
||||
char fix_character_one(char digit) {
|
||||
char return_char = digit; // We don't need to update this for 0, 1, 3, 7 and 8.
|
||||
switch(digit) {
|
||||
case '2':
|
||||
// Roman numeral / tally representation of 2
|
||||
return_char = '|'; // Thanks, Joey, for already having this in the character set.
|
||||
break;
|
||||
case '4':
|
||||
// Looks almost like a 4 - just missing the top-left segment.
|
||||
// 0b01000110
|
||||
return_char = '&'; // Slight hack - I want 0b01000110, but 0b01000100 is already in the character set and will do, since B and C segments are linked in this position.
|
||||
break;
|
||||
case '5':
|
||||
return_char = 'F'; // F for Five
|
||||
break;
|
||||
case '6':
|
||||
return_char = 'E'; // Looks almost like a 6 - just missing the bottom-right segment. Not super happy with it, but liked it best of the options I tried.
|
||||
break;
|
||||
case '9':
|
||||
return_char = 'N'; // N for Nine
|
||||
break;
|
||||
}
|
||||
return return_char;
|
||||
}
|
84
movement/watch_faces/clock/french_revolutionary_face.h
Normal file
84
movement/watch_faces/clock/french_revolutionary_face.h
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 CarpeNoctem
|
||||
*
|
||||
* 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 FRENCH_REVOLUTIONARY_FACE_H_
|
||||
#define FRENCH_REVOLUTIONARY_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* French Revolutionary Decimal Time
|
||||
*
|
||||
* Similar to the Decimal Time face, but with the day divided into ten hours instead of twenty four.
|
||||
* Each hour is divided into one hundred minutes, and those minutes are divided into 100 seconds.
|
||||
* I came across this one the Svalbard watch site here: https://svalbard.watch/pages/about_decimal_time.html
|
||||
* More info here as well: https://en.wikipedia.org/wiki/Decimal_time
|
||||
*
|
||||
* By default, the face just displays the current decimal time. Pressing the alarm button will toggle through other display options:
|
||||
* 1) Just decimal time (with dT indicator at top)
|
||||
* 2) Decimal time, with dT indicator and date above.
|
||||
* 3) Decimal time, with 24-hr time above (where Day and Date would normally be displayed), BUT minutes first then hours.
|
||||
* Sadly, the first character of the date area only goes up to 3 (see https://www.sensorwatch.net/docs/wig/display/#the-day-digits)
|
||||
* I was going to begrudgindly leave this display option out when I realized that, but thought it would be better to have this backwards
|
||||
* representation of the "normal" time than not at all.
|
||||
*
|
||||
* A long-press of the light button will toggle the upper time between 12-hr AM/PM and 24-hr mode. I thought of reading the main setting for this,
|
||||
* but thought that a person could normally prefer 12hr time, but next to a 10hr day want to see the normal time in the 24hr format.
|
||||
*
|
||||
* A long-press of the alarm button will toggle the seconds off and on.
|
||||
*
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
bool use_am_pm; // Use 12-hr AM/PM for upper display instead of 24-hr? (Default is 24-hr)
|
||||
bool show_seconds;
|
||||
bool colon_set_after_splash;
|
||||
uint8_t display_type : 2;
|
||||
} french_revolutionary_state_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t second : 8; // 0-99
|
||||
uint8_t minute : 8; // 0-99
|
||||
uint8_t hour : 5; // 0-10
|
||||
} fr_decimal_time;
|
||||
|
||||
void french_revolutionary_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void french_revolutionary_face_activate(movement_settings_t *settings, void *context);
|
||||
bool french_revolutionary_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void french_revolutionary_face_resign(movement_settings_t *settings, void *context);
|
||||
char fix_character_one(char digit);
|
||||
fr_decimal_time get_decimal_time(watch_date_time *date_time);
|
||||
void set_display_buffer(char *buf, french_revolutionary_state_t *state, fr_decimal_time *decimal_time, watch_date_time *date_time);
|
||||
|
||||
|
||||
#define french_revolutionary_face ((const watch_face_t){ \
|
||||
french_revolutionary_face_setup, \
|
||||
french_revolutionary_face_activate, \
|
||||
french_revolutionary_face_loop, \
|
||||
french_revolutionary_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // FRENCH_REVOLUTIONARY_FACE_H_
|
||||
|
117
movement/watch_faces/clock/minimal_clock_face.c
Normal file
117
movement/watch_faces/clock/minimal_clock_face.c
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Dennisman219
|
||||
*
|
||||
* 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 "minimal_clock_face.h"
|
||||
|
||||
static void _minimal_clock_face_update_display(movement_settings_t *settings) {
|
||||
watch_date_time date_time = watch_rtc_get_date_time();
|
||||
char buffer[11];
|
||||
|
||||
if (!settings->bit.clock_mode_24h) {
|
||||
date_time.unit.hour %= 12;
|
||||
sprintf(buffer, "%2d%02d ", date_time.unit.hour, date_time.unit.minute);
|
||||
} else {
|
||||
sprintf(buffer, "%02d%02d ", date_time.unit.hour, date_time.unit.minute);
|
||||
}
|
||||
|
||||
watch_display_string(buffer, 4);
|
||||
}
|
||||
|
||||
void minimal_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(minimal_clock_state_t));
|
||||
memset(*context_ptr, 0, sizeof(minimal_clock_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 minimal_clock_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 minimal_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
(void) context;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
// Show your initial UI here.
|
||||
_minimal_clock_face_update_display(settings);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
// If needed, update your display here.
|
||||
_minimal_clock_face_update_display(settings);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
// You can use the Light button for your own purposes. Note that by default, Movement will also
|
||||
// illuminate the LED in response to EVENT_LIGHT_BUTTON_DOWN; to suppress that behavior, add an
|
||||
// empty case for EVENT_LIGHT_BUTTON_DOWN.
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
// Just in case you have need for another button.
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
// Your watch face will receive this event after a period of inactivity. If it makes sense to resign,
|
||||
// you may uncomment this line to move back to the first watch face in the list:
|
||||
// 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);
|
||||
_minimal_clock_face_update_display(settings);
|
||||
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 minimal_clock_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
|
||||
// handle any cleanup before your watch face goes off-screen.
|
||||
}
|
||||
|
57
movement/watch_faces/clock/minimal_clock_face.h
Normal file
57
movement/watch_faces/clock/minimal_clock_face.h
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Dennisman219
|
||||
*
|
||||
* 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 MINIMAL_CLOCK_FACE_H_
|
||||
#define MINIMAL_CLOCK_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* MINIMAL CLOCK FACE
|
||||
*
|
||||
* A minimal clock face that just shows hours and minutes.
|
||||
* There is nothing to configure. The face follows the 12h/24h setting
|
||||
*
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
// Anything you need to keep track of, put it here!
|
||||
uint8_t unused;
|
||||
} minimal_clock_state_t;
|
||||
|
||||
void minimal_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void minimal_clock_face_activate(movement_settings_t *settings, void *context);
|
||||
bool minimal_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void minimal_clock_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define minimal_clock_face ((const watch_face_t){ \
|
||||
minimal_clock_face_setup, \
|
||||
minimal_clock_face_activate, \
|
||||
minimal_clock_face_loop, \
|
||||
minimal_clock_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // MINIMAL_CLOCK_FACE_H_
|
||||
|
|
@ -68,7 +68,7 @@ void repetition_minute_face_activate(movement_settings_t *settings, void *contex
|
|||
|
||||
if (watch_tick_animation_is_running()) watch_stop_tick_animation();
|
||||
|
||||
if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
|
||||
// handle chime indicator
|
||||
if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
|
@ -112,6 +112,7 @@ bool repetition_minute_face_loop(movement_event_t event, movement_settings_t *se
|
|||
// ...and set the LAP indicator if low.
|
||||
if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP);
|
||||
|
||||
bool set_leading_zero = false;
|
||||
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);
|
||||
|
@ -132,6 +133,8 @@ bool repetition_minute_face_loop(movement_event_t event, movement_settings_t *se
|
|||
}
|
||||
date_time.unit.hour %= 12;
|
||||
if (date_time.unit.hour == 0) date_time.unit.hour = 12;
|
||||
} else if (settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
pos = 0;
|
||||
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
|
||||
|
@ -142,6 +145,8 @@ bool repetition_minute_face_loop(movement_event_t event, movement_settings_t *se
|
|||
}
|
||||
}
|
||||
watch_display_string(buf, pos);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
// handle alarm indicator
|
||||
if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state);
|
||||
break;
|
||||
|
|
|
@ -60,7 +60,7 @@ void simple_clock_bin_led_face_activate(movement_settings_t *settings, void *con
|
|||
|
||||
if (watch_tick_animation_is_running()) watch_stop_tick_animation();
|
||||
|
||||
if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
|
||||
// handle chime indicator
|
||||
if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
|
@ -138,6 +138,7 @@ bool simple_clock_bin_led_face_loop(movement_event_t event, movement_settings_t
|
|||
// ...and set the LAP indicator if low.
|
||||
if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP);
|
||||
|
||||
bool set_leading_zero = false;
|
||||
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);
|
||||
|
@ -158,6 +159,8 @@ bool simple_clock_bin_led_face_loop(movement_event_t event, movement_settings_t
|
|||
}
|
||||
date_time.unit.hour %= 12;
|
||||
if (date_time.unit.hour == 0) date_time.unit.hour = 12;
|
||||
} else if (settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
pos = 0;
|
||||
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
|
||||
|
@ -168,6 +171,8 @@ bool simple_clock_bin_led_face_loop(movement_event_t event, movement_settings_t
|
|||
}
|
||||
}
|
||||
watch_display_string(buf, pos);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
// handle alarm indicator
|
||||
if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state);
|
||||
}
|
||||
|
|
|
@ -51,7 +51,11 @@ void simple_clock_face_activate(movement_settings_t *settings, void *context) {
|
|||
|
||||
if (watch_tick_animation_is_running()) watch_stop_tick_animation();
|
||||
|
||||
#ifdef CLOCK_FACE_24H_ONLY
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
#else
|
||||
if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
#endif
|
||||
|
||||
// handle chime indicator
|
||||
if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
|
@ -95,6 +99,7 @@ bool simple_clock_face_loop(movement_event_t event, movement_settings_t *setting
|
|||
// ...and set the LAP indicator if low.
|
||||
if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP);
|
||||
|
||||
bool set_leading_zero = false;
|
||||
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);
|
||||
|
@ -106,6 +111,7 @@ bool simple_clock_face_loop(movement_event_t event, movement_settings_t *setting
|
|||
sprintf(buf, "%02d%02d", date_time.unit.minute, date_time.unit.second);
|
||||
} else {
|
||||
// other stuff changed; let's do it all.
|
||||
#ifndef CLOCK_FACE_24H_ONLY
|
||||
if (!settings->bit.clock_mode_24h) {
|
||||
// if we are in 12 hour mode, do some cleanup.
|
||||
if (date_time.unit.hour < 12) {
|
||||
|
@ -116,6 +122,12 @@ bool simple_clock_face_loop(movement_event_t event, movement_settings_t *setting
|
|||
date_time.unit.hour %= 12;
|
||||
if (date_time.unit.hour == 0) date_time.unit.hour = 12;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (settings->bit.clock_mode_24h && settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
|
||||
pos = 0;
|
||||
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
|
||||
if (!watch_tick_animation_is_running()) watch_start_tick_animation(500);
|
||||
|
@ -125,6 +137,10 @@ bool simple_clock_face_loop(movement_event_t event, movement_settings_t *setting
|
|||
}
|
||||
}
|
||||
watch_display_string(buf, pos);
|
||||
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
|
||||
// handle alarm indicator
|
||||
if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state);
|
||||
break;
|
||||
|
|
|
@ -50,7 +50,7 @@ void weeknumber_clock_face_activate(movement_settings_t *settings, void *context
|
|||
|
||||
if (watch_tick_animation_is_running()) watch_stop_tick_animation();
|
||||
|
||||
if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
|
||||
// handle chime indicator
|
||||
if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
|
@ -94,6 +94,7 @@ bool weeknumber_clock_face_loop(movement_event_t event, movement_settings_t *set
|
|||
// ...and set the LAP indicator if low.
|
||||
if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP);
|
||||
|
||||
bool set_leading_zero = false;
|
||||
if ((date_time.reg >> 12) == (previous_date_time >> 12) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
|
||||
// everything before minutes is the same.
|
||||
pos = 6;
|
||||
|
@ -109,6 +110,8 @@ bool weeknumber_clock_face_loop(movement_event_t event, movement_settings_t *set
|
|||
}
|
||||
date_time.unit.hour %= 12;
|
||||
if (date_time.unit.hour == 0) date_time.unit.hour = 12;
|
||||
} else if (settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
pos = 0;
|
||||
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
|
||||
|
@ -119,6 +122,8 @@ bool weeknumber_clock_face_loop(movement_event_t event, movement_settings_t *set
|
|||
}
|
||||
}
|
||||
watch_display_string(buf, pos);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
// handle alarm indicator
|
||||
if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state);
|
||||
break;
|
||||
|
|
|
@ -174,7 +174,7 @@ static bool mode_display(movement_event_t event, movement_settings_t *settings,
|
|||
if (refresh_face) {
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
watch_set_colon();
|
||||
if (settings->bit.clock_mode_24h)
|
||||
if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero)
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
|
||||
state->previous_date_time = REFRESH_TIME;
|
||||
|
@ -188,6 +188,7 @@ static bool mode_display(movement_event_t event, movement_settings_t *settings,
|
|||
previous_date_time = state->previous_date_time;
|
||||
state->previous_date_time = date_time.reg;
|
||||
|
||||
bool set_leading_zero = false;
|
||||
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. */
|
||||
pos = 8;
|
||||
|
@ -208,6 +209,8 @@ static bool mode_display(movement_event_t event, movement_settings_t *settings,
|
|||
date_time.unit.hour %= 12;
|
||||
if (date_time.unit.hour == 0)
|
||||
date_time.unit.hour = 12;
|
||||
} else if (settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
|
||||
pos = 0;
|
||||
|
@ -230,6 +233,8 @@ static bool mode_display(movement_event_t event, movement_settings_t *settings,
|
|||
}
|
||||
}
|
||||
watch_display_string(buf, pos);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
state->current_zone = find_selected_zone(state, FORWARD);
|
||||
|
|
|
@ -60,7 +60,7 @@ static bool world_clock_face_do_display_mode(movement_event_t event, movement_se
|
|||
watch_date_time date_time;
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
watch_set_colon();
|
||||
state->previous_date_time = 0xFFFFFFFF;
|
||||
// fall through
|
||||
|
@ -72,6 +72,7 @@ static bool world_clock_face_do_display_mode(movement_event_t event, movement_se
|
|||
previous_date_time = state->previous_date_time;
|
||||
state->previous_date_time = date_time.reg;
|
||||
|
||||
bool set_leading_zero = false;
|
||||
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.
|
||||
pos = 8;
|
||||
|
@ -91,6 +92,8 @@ static bool world_clock_face_do_display_mode(movement_event_t event, movement_se
|
|||
}
|
||||
date_time.unit.hour %= 12;
|
||||
if (date_time.unit.hour == 0) date_time.unit.hour = 12;
|
||||
} else if (settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
pos = 0;
|
||||
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
|
||||
|
@ -112,6 +115,8 @@ static bool world_clock_face_do_display_mode(movement_event_t event, movement_se
|
|||
}
|
||||
}
|
||||
watch_display_string(buf, pos);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
movement_request_tick_frequency(4);
|
||||
|
|
|
@ -293,6 +293,7 @@ static void _activity_update_logging_screen(movement_settings_t *settings, activ
|
|||
}
|
||||
// Briefly, show time without seconds
|
||||
else {
|
||||
bool set_leading_zero = false;
|
||||
watch_clear_indicator(WATCH_INDICATOR_LAP);
|
||||
watch_date_time now = watch_rtc_get_date_time();
|
||||
uint8_t hour = now.unit.hour;
|
||||
|
@ -304,14 +305,18 @@ static void _activity_update_logging_screen(movement_settings_t *settings, activ
|
|||
watch_set_indicator(WATCH_INDICATOR_PM);
|
||||
hour %= 12;
|
||||
if (hour == 0) hour = 12;
|
||||
}
|
||||
else {
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
if (!settings->bit.clock_24h_leading_zero)
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
else if (hour < 10)
|
||||
set_leading_zero = true;
|
||||
}
|
||||
sprintf(activity_buf, "%2d%02d ", hour, now.unit.minute);
|
||||
watch_set_colon();
|
||||
watch_display_string(activity_buf, 4);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ static void _alarm_face_draw(movement_settings_t *settings, alarm_state_t *state
|
|||
i = state->alarm[state->alarm_idx].day + 1;
|
||||
}
|
||||
//handle am/pm for hour display
|
||||
bool set_leading_zero = false;
|
||||
uint8_t h = state->alarm[state->alarm_idx].hour;
|
||||
if (!settings->bit.clock_mode_24h) {
|
||||
if (h >= 12) {
|
||||
|
@ -81,8 +82,17 @@ static void _alarm_face_draw(movement_settings_t *settings, alarm_state_t *state
|
|||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
}
|
||||
if (h == 0) h = 12;
|
||||
} else {
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
|
||||
if (settings->bit.clock_24h_leading_zero) {
|
||||
if (h < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
sprintf(buf, "%c%c%2d%2d%02d ",
|
||||
}
|
||||
}
|
||||
|
||||
sprintf(buf, set_leading_zero? "%c%c%2d%02d%02d " : "%c%c%2d%2d%02d ",
|
||||
_dow_strings[i][0], _dow_strings[i][1],
|
||||
(state->alarm_idx + 1),
|
||||
h,
|
||||
|
|
467
movement/watch_faces/complication/butterfly_game_face.c
Normal file
467
movement/watch_faces/complication/butterfly_game_face.c
Normal file
|
@ -0,0 +1,467 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Hugo Chargois
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Emulator only: need time() to seed the random number generator
|
||||
#if __EMSCRIPTEN__
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "butterfly_game_face.h"
|
||||
|
||||
static char butterfly_shapes[][3] = {
|
||||
"[]", "][", "25", "52", "9e", "e9", "6a", "a6", "3E", "E3", "00", "HH", "88"
|
||||
};
|
||||
|
||||
static int8_t single_beep[] = {BUZZER_NOTE_A7, 4, 0};
|
||||
static int8_t round_win_melody[] = {
|
||||
BUZZER_NOTE_C6, 4,
|
||||
BUZZER_NOTE_E6, 4,
|
||||
BUZZER_NOTE_G6, 4,
|
||||
BUZZER_NOTE_C7, 12,
|
||||
0};
|
||||
static int8_t round_lose_melody[] = {
|
||||
BUZZER_NOTE_E6, 4,
|
||||
BUZZER_NOTE_F6, 4,
|
||||
BUZZER_NOTE_D6SHARP_E6FLAT, 4,
|
||||
BUZZER_NOTE_C6, 12,
|
||||
0};
|
||||
static int8_t game_win_melody[] = {
|
||||
BUZZER_NOTE_G6, 4,
|
||||
BUZZER_NOTE_A6, 4,
|
||||
BUZZER_NOTE_B6, 4,
|
||||
BUZZER_NOTE_C7, 12,
|
||||
BUZZER_NOTE_D7, 4,
|
||||
BUZZER_NOTE_E7, 4,
|
||||
BUZZER_NOTE_D7, 4,
|
||||
BUZZER_NOTE_C7, 12,
|
||||
BUZZER_NOTE_B6, 4,
|
||||
BUZZER_NOTE_C7, 4,
|
||||
BUZZER_NOTE_D7, 4,
|
||||
BUZZER_NOTE_G7, 24,
|
||||
0};
|
||||
|
||||
#define NUM_SHAPES (sizeof(butterfly_shapes) / sizeof(butterfly_shapes[0]))
|
||||
|
||||
#define POS_LEFT 4
|
||||
#define POS_CENTER 6
|
||||
#define POS_RIGHT 8
|
||||
|
||||
#define TICK_FREQ 8
|
||||
#define TICKS_PER_SHAPE 8
|
||||
|
||||
#define PLAYER_1 0
|
||||
#define PLAYER_2 1
|
||||
|
||||
|
||||
// returns a random integer r with 0 <= r < max
|
||||
static inline uint8_t _get_rand(uint8_t max) {
|
||||
#if __EMSCRIPTEN__
|
||||
return rand() % max;
|
||||
#else
|
||||
return arc4random_uniform(max);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* The game is built with a simple state machine where each state is called a
|
||||
* "screen". Each screen can draw on the display and handles events, including
|
||||
* the "activate" event, which is repurposed and sent whenever we move from one
|
||||
* screen to another via the _transition_to function. Basically it's a mini
|
||||
* movement inside movement.
|
||||
*/
|
||||
typedef bool (*screen_fn_t)(movement_event_t, butterfly_game_state_t*);
|
||||
|
||||
static screen_fn_t cur_screen_fn;
|
||||
|
||||
static bool _transition_to(screen_fn_t sf, butterfly_game_state_t *state) {
|
||||
movement_event_t ev = {EVENT_ACTIVATE, 0};
|
||||
cur_screen_fn = sf;
|
||||
return sf(ev, state);
|
||||
}
|
||||
|
||||
static uint8_t _pick_wrong_shape(butterfly_game_state_t *state, bool skip_wrong_shape) {
|
||||
if (!skip_wrong_shape) {
|
||||
// easy case, we only need to skip over 1 shape: the correct shape
|
||||
uint8_t r = _get_rand(NUM_SHAPES-1);
|
||||
if (r >= state->correct_shape) {
|
||||
r++;
|
||||
}
|
||||
return r;
|
||||
} else {
|
||||
// a bit more complex, we need to skip over 2 shapes: the correct one
|
||||
// and the current wrong one
|
||||
uint8_t r = _get_rand(NUM_SHAPES-2);
|
||||
uint8_t i1, i2; // the 2 indices to skip over, with i1 < i2
|
||||
if (state->correct_shape < state->current_shape) {
|
||||
i1 = state->correct_shape;
|
||||
i2 = state->current_shape;
|
||||
} else {
|
||||
i1 = state->current_shape;
|
||||
i2 = state->correct_shape;
|
||||
}
|
||||
|
||||
if (r >= i1) {
|
||||
r++;
|
||||
}
|
||||
if (r >= i2) {
|
||||
r++;
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
static void _display_shape(uint8_t shape, uint8_t pos) {
|
||||
watch_display_string(butterfly_shapes[shape], pos);
|
||||
}
|
||||
|
||||
static void _display_scores(butterfly_game_state_t *state) {
|
||||
char buf[] = " ";
|
||||
buf[0] = '0' + state->score_p1;
|
||||
watch_display_string(buf, 0);
|
||||
buf[0] = '0' + state->score_p2;
|
||||
watch_display_string(buf, 3);
|
||||
}
|
||||
|
||||
static void _play_sound(butterfly_game_state_t *state, int8_t *seq) {
|
||||
if (state->sound) watch_buzzer_play_sequence(seq, NULL);
|
||||
}
|
||||
|
||||
static bool _round_start_screen(movement_event_t event, butterfly_game_state_t *state);
|
||||
static bool _reset_screen(movement_event_t event, butterfly_game_state_t *state);
|
||||
|
||||
static bool _game_win_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->ctr = 4 * TICK_FREQ;
|
||||
watch_clear_display();
|
||||
|
||||
if (state->score_p1 >= state->goal_score) {
|
||||
watch_display_string("pl1 wins", 0);
|
||||
} else {
|
||||
watch_display_string("pl2 wins", 0);
|
||||
}
|
||||
_play_sound(state, game_win_melody);
|
||||
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
state->ctr--;
|
||||
if (state->ctr == 0) {
|
||||
return _transition_to(_reset_screen, state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _round_win_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->ctr = TICK_FREQ;
|
||||
|
||||
if (state->round_winner == PLAYER_1) {
|
||||
state->score_p1++;
|
||||
} else {
|
||||
state->score_p2++;
|
||||
}
|
||||
|
||||
watch_clear_display();
|
||||
_display_scores(state);
|
||||
_display_shape(state->correct_shape, state->round_winner == PLAYER_1 ? POS_LEFT : POS_RIGHT);
|
||||
_play_sound(state, round_win_melody);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
state->ctr--;
|
||||
if (state->ctr == 0) {
|
||||
if (state->score_p1 >= state->goal_score || state->score_p2 >= state->goal_score) {
|
||||
return _transition_to(_game_win_screen, state);
|
||||
}
|
||||
return _transition_to(_round_start_screen, state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _round_lose_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->ctr = TICK_FREQ;
|
||||
|
||||
if (state->round_winner == PLAYER_1) {
|
||||
if (state->score_p2 > 0) state->score_p2--;
|
||||
} else {
|
||||
if (state->score_p1 > 0) state->score_p1--;
|
||||
}
|
||||
|
||||
_display_shape(state->correct_shape, POS_CENTER);
|
||||
_play_sound(state, round_lose_melody);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (--state->ctr == 0) {
|
||||
return _transition_to(_round_start_screen, state);
|
||||
}
|
||||
_display_shape(state->ctr%2 ? state->correct_shape : state->current_shape, POS_CENTER);
|
||||
break;
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _correct_shape_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
_display_shape(state->correct_shape, POS_CENTER);
|
||||
_play_sound(state, single_beep);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
state->round_winner = PLAYER_1;
|
||||
return _transition_to(_round_win_screen, state);
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->round_winner = PLAYER_2;
|
||||
return _transition_to(_round_win_screen, state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _wrong_shape_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->ctr = TICKS_PER_SHAPE;
|
||||
state->current_shape = _pick_wrong_shape(state, true);
|
||||
_display_shape(state->current_shape, POS_CENTER);
|
||||
_play_sound(state, single_beep);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (--state->ctr == 0) {
|
||||
if (--state->show_correct_shape_after == 0) {
|
||||
return _transition_to(_correct_shape_screen, state);
|
||||
}
|
||||
return _transition_to(_wrong_shape_screen, state);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
state->round_winner = PLAYER_2;
|
||||
return _transition_to(_round_lose_screen, state);
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->round_winner = PLAYER_1;
|
||||
return _transition_to(_round_lose_screen, state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _first_wrong_shape_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
// the first of the wrong shape screens is a bit different than the next
|
||||
// ones, for 2 reasons:
|
||||
// * we can pick any shape except one (the correct shape); whereas in the
|
||||
// subsequent wrong shape screens, we also must not pick the same wrong
|
||||
// shape as the last
|
||||
// * we don't act on the light/alarm button events; they would normally be
|
||||
// a fail in a wrong shape screen, but in this case it may just be that
|
||||
// the 2 players acknowledge the picked shape (in the previous screen) in
|
||||
// quick succession, and we don't want the second player to immediately
|
||||
// fail.
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->ctr = TICKS_PER_SHAPE;
|
||||
state->current_shape = _pick_wrong_shape(state, false);
|
||||
_display_shape(state->current_shape, POS_CENTER);
|
||||
_play_sound(state, single_beep);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (--state->ctr == 0) {
|
||||
return _transition_to(_wrong_shape_screen, state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _round_start_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->correct_shape = _get_rand(NUM_SHAPES);
|
||||
state->show_correct_shape_after = _get_rand(10) + 1;
|
||||
watch_display_string(" - -", 0);
|
||||
_display_scores(state);
|
||||
_display_shape(state->correct_shape, POS_CENTER);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
watch_display_string(" ", 4);
|
||||
return _transition_to(_first_wrong_shape_screen, state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _goal_select_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
watch_clear_display();
|
||||
state->goal_score = 6;
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
return _transition_to(_round_start_screen, state);
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->goal_score += 3;
|
||||
if (state->goal_score > 9) state->goal_score = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
char buf[] = "GOaL ";
|
||||
buf[5] = '0' + state->goal_score;
|
||||
watch_display_string(buf, 4);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _reset_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
(void) event;
|
||||
|
||||
state->score_p1 = 0;
|
||||
state->score_p2 = 0;
|
||||
|
||||
return _transition_to(_goal_select_screen, state);
|
||||
}
|
||||
|
||||
static bool _continue_select_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
watch_clear_display();
|
||||
|
||||
// no game in progress, start a new game
|
||||
if (state->score_p1 == 0 && state->score_p2 == 0) {
|
||||
return _transition_to(_goal_select_screen, state);
|
||||
}
|
||||
|
||||
state->cont = false;
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
if (state->cont) {
|
||||
return _transition_to(_round_start_screen, state);
|
||||
}
|
||||
return _transition_to(_reset_screen, state);
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->cont = !state->cont;
|
||||
break;
|
||||
}
|
||||
|
||||
if (state->cont) {
|
||||
watch_display_string("Cont y", 4);
|
||||
} else {
|
||||
watch_display_string("Cont n", 4);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _sound_select_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
watch_clear_display();
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
return _transition_to(_continue_select_screen, state);
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->sound = !state->sound;
|
||||
break;
|
||||
}
|
||||
|
||||
if (state->sound) {
|
||||
watch_display_string("snd y", 5);
|
||||
} else {
|
||||
watch_display_string("snd n", 5);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _splash_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->ctr = TICK_FREQ;
|
||||
|
||||
watch_clear_display();
|
||||
watch_display_string("Btrfly", 4);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
return _transition_to(_sound_select_screen, state);
|
||||
case EVENT_TICK:
|
||||
if (--state->ctr == 0) {
|
||||
return _transition_to(_sound_select_screen, state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void butterfly_game_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(butterfly_game_state_t));
|
||||
memset(*context_ptr, 0, sizeof(butterfly_game_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.
|
||||
#if __EMSCRIPTEN__
|
||||
// simulator only: seed the random number generator
|
||||
time_t t;
|
||||
srand((unsigned) time(&t));
|
||||
#endif
|
||||
}
|
||||
|
||||
void butterfly_game_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
|
||||
movement_request_tick_frequency(TICK_FREQ);
|
||||
}
|
||||
|
||||
bool butterfly_game_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
butterfly_game_state_t *state = (butterfly_game_state_t *)context;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
return _transition_to(_splash_screen, state);
|
||||
case EVENT_TICK:
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
return (*cur_screen_fn)(event, state);
|
||||
case EVENT_TIMEOUT:
|
||||
movement_move_to_face(0);
|
||||
return true;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
}
|
||||
|
||||
void butterfly_game_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
|
||||
// handle any cleanup before your watch face goes off-screen.
|
||||
}
|
||||
|
125
movement/watch_faces/complication/butterfly_game_face.h
Normal file
125
movement/watch_faces/complication/butterfly_game_face.h
Normal file
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Hugo Chargois
|
||||
*
|
||||
* 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 BUTTERFLY_GAME_FACE_H_
|
||||
#define BUTTERFLY_GAME_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* BUTTERFLY
|
||||
*
|
||||
* A GAME OF SHAPE RECOGNITION AND QUICK REFLEXES FOR 2 PLAYERS
|
||||
*
|
||||
* Setup
|
||||
* =====
|
||||
*
|
||||
* The game is played by 2 players, each using a distinct button:
|
||||
* - player 1 plays with the LIGHT (upper left) button
|
||||
* - player 2 plays with the ALARM (lower right) button
|
||||
*
|
||||
* To play, both players need a firm grip on the watch. A suggested method is to
|
||||
* face each other, remove the watch from the wrist, and position it sideways
|
||||
* between you. Hold one side of the strap in your preferred hand (right or
|
||||
* left) and use your thumb to play.
|
||||
*
|
||||
* Start of the game
|
||||
* =================
|
||||
*
|
||||
* After the splash screen (BtrFly) is shown, the game proceeds through a couple
|
||||
* configuration screens. Use ALARM to cycle through the possible values, and
|
||||
* LIGHT to validate and move to the next screen.
|
||||
*
|
||||
* The configuration options are:
|
||||
*
|
||||
* - snd y/n Toggle sound effects on or off
|
||||
* - goal 3/6/9 Choose to play a game of 3, 6 or 9 points
|
||||
* - cont y/n Decide to continue an unfinished game or start a new one
|
||||
* (this option appears only if a game is in progress)
|
||||
*
|
||||
* Rules
|
||||
* =====
|
||||
*
|
||||
* Prior to each round, a symmetrical shape composed of 2 characters is shown in
|
||||
* the center of the screen. This shape, representing a butterfly's wings, is
|
||||
* randomly chosen from a set of a dozen or so possible shapes. For example:
|
||||
*
|
||||
* ][
|
||||
*
|
||||
* Memorize this shape! Your objective in the round will be to "catch" this
|
||||
* "butterfly" by pressing your button before your opponent does.
|
||||
*
|
||||
* Once you believe you've memorized the shape, press your button. The round
|
||||
* officially begins as soon as either player presses their button.
|
||||
*
|
||||
* Various "butterflies" will then appear on the screen, one after the other.
|
||||
* The fastest player to press their button when the correct butterfly is shown
|
||||
* wins the round. However, if a player presses their button when an incorrect
|
||||
* butterfly is shown, they immediately lose the round.
|
||||
*
|
||||
* Scoring
|
||||
* =======
|
||||
*
|
||||
* The scores are displayed at the top of the screen at all times.
|
||||
*
|
||||
* When a round is won by a player, their score increases by one. When a round
|
||||
* is lost by a player, their score decreases by one; unless they have a score
|
||||
* of 0, in which case it remains unchanged.
|
||||
*
|
||||
* The game ends when a player reaches the set point goal (3, 6 or 9 points).
|
||||
*
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
bool cont : 1; // continue
|
||||
bool sound : 1;
|
||||
uint8_t goal_score : 4;
|
||||
|
||||
// a generic ctr used by multiple states to display themselves for multiple frames
|
||||
uint8_t ctr : 6;
|
||||
|
||||
uint8_t correct_shape : 5;
|
||||
uint8_t current_shape : 5;
|
||||
uint8_t show_correct_shape_after : 5;
|
||||
uint8_t round_winner : 1;
|
||||
|
||||
uint8_t score_p1 : 5;
|
||||
uint8_t score_p2 : 5;
|
||||
} butterfly_game_state_t;
|
||||
|
||||
void butterfly_game_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void butterfly_game_face_activate(movement_settings_t *settings, void *context);
|
||||
bool butterfly_game_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void butterfly_game_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define butterfly_game_face ((const watch_face_t){ \
|
||||
butterfly_game_face_setup, \
|
||||
butterfly_game_face_activate, \
|
||||
butterfly_game_face_loop, \
|
||||
butterfly_game_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // BUTTERFLY_GAME_FACE_H_
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Joseph Bryant
|
||||
* Copyright (c) 2023 Konrad Rieck
|
||||
* Copyright (c) 2022 Wesley Ellis
|
||||
*
|
||||
|
@ -68,17 +69,30 @@ static inline void button_beep(movement_settings_t *settings) {
|
|||
watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
|
||||
}
|
||||
|
||||
static void start(countdown_state_t *state, movement_settings_t *settings) {
|
||||
watch_date_time now = watch_rtc_get_date_time();
|
||||
static void schedule_countdown(countdown_state_t *state, movement_settings_t *settings) {
|
||||
|
||||
state->mode = cd_running;
|
||||
state->now_ts = watch_utility_date_time_to_unix_time(now, get_tz_offset(settings));
|
||||
state->target_ts = watch_utility_offset_timestamp(state->now_ts, state->hours, state->minutes, state->seconds);
|
||||
// Calculate the new state->now_ts but don't update it until we've updated the target -
|
||||
// avoid possible race where the old target is compared to the new time and immediately triggers
|
||||
uint32_t new_now = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), get_tz_offset(settings));
|
||||
state->target_ts = watch_utility_offset_timestamp(new_now, state->hours, state->minutes, state->seconds);
|
||||
state->now_ts = new_now;
|
||||
watch_date_time target_dt = watch_utility_date_time_from_unix_time(state->target_ts, get_tz_offset(settings));
|
||||
movement_schedule_background_task(target_dt);
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
movement_schedule_background_task_for_face(state->watch_face_index, target_dt);
|
||||
}
|
||||
|
||||
static void auto_repeat(countdown_state_t *state, movement_settings_t *settings) {
|
||||
movement_play_alarm();
|
||||
load_countdown(state);
|
||||
schedule_countdown(state, settings);
|
||||
}
|
||||
|
||||
static void start(countdown_state_t *state, movement_settings_t *settings) {
|
||||
state->mode = cd_running;
|
||||
schedule_countdown(state, settings);
|
||||
}
|
||||
|
||||
|
||||
|
||||
static void draw(countdown_state_t *state, uint8_t subsecond) {
|
||||
char buf[16];
|
||||
|
||||
|
@ -87,6 +101,9 @@ static void draw(countdown_state_t *state, uint8_t subsecond) {
|
|||
|
||||
switch (state->mode) {
|
||||
case cd_running:
|
||||
if (state->target_ts <= state->now_ts)
|
||||
delta = 0;
|
||||
else
|
||||
delta = state->target_ts - state->now_ts;
|
||||
result = div(delta, 60);
|
||||
state->seconds = result.rem;
|
||||
|
@ -97,6 +114,7 @@ static void draw(countdown_state_t *state, uint8_t subsecond) {
|
|||
break;
|
||||
case cd_reset:
|
||||
case cd_paused:
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
sprintf(buf, "CD %2d%02d%02d", state->hours, state->minutes, state->seconds);
|
||||
break;
|
||||
case cd_setting:
|
||||
|
@ -123,14 +141,13 @@ static void draw(countdown_state_t *state, uint8_t subsecond) {
|
|||
|
||||
static void pause(countdown_state_t *state) {
|
||||
state->mode = cd_paused;
|
||||
movement_cancel_background_task();
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
movement_cancel_background_task_for_face(state->watch_face_index);
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
}
|
||||
|
||||
static void reset(countdown_state_t *state) {
|
||||
state->mode = cd_reset;
|
||||
movement_cancel_background_task();
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
movement_cancel_background_task_for_face(state->watch_face_index);
|
||||
load_countdown(state);
|
||||
}
|
||||
|
||||
|
@ -139,6 +156,15 @@ static void ring(countdown_state_t *state) {
|
|||
reset(state);
|
||||
}
|
||||
|
||||
static void times_up(movement_settings_t *settings, countdown_state_t *state) {
|
||||
if(state->repeat) {
|
||||
auto_repeat(state, settings);
|
||||
}
|
||||
else {
|
||||
ring(state);
|
||||
}
|
||||
}
|
||||
|
||||
static void settings_increment(countdown_state_t *state) {
|
||||
switch(state->selection) {
|
||||
case 0:
|
||||
|
@ -167,6 +193,7 @@ void countdown_face_setup(movement_settings_t *settings, uint8_t watch_face_inde
|
|||
memset(*context_ptr, 0, sizeof(countdown_state_t));
|
||||
state->minutes = DEFAULT_MINUTES;
|
||||
state->mode = cd_reset;
|
||||
state->watch_face_index = watch_face_index;
|
||||
store_countdown(state);
|
||||
}
|
||||
}
|
||||
|
@ -177,9 +204,11 @@ void countdown_face_activate(movement_settings_t *settings, void *context) {
|
|||
if(state->mode == cd_running) {
|
||||
watch_date_time now = watch_rtc_get_date_time();
|
||||
state->now_ts = watch_utility_date_time_to_unix_time(now, get_tz_offset(settings));
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
}
|
||||
watch_set_colon();
|
||||
if(state->repeat)
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
|
||||
movement_request_tick_frequency(1);
|
||||
quick_ticks_running = false;
|
||||
|
@ -249,6 +278,7 @@ bool countdown_face_loop(movement_event_t event, movement_settings_t *settings,
|
|||
// Only start the timer if we have a valid time.
|
||||
start(state, settings);
|
||||
button_beep(settings);
|
||||
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
}
|
||||
break;
|
||||
case cd_setting:
|
||||
|
@ -258,9 +288,19 @@ bool countdown_face_loop(movement_event_t event, movement_settings_t *settings,
|
|||
draw(state, event.subsecond);
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (state->mode == cd_setting) {
|
||||
switch(state->mode) {
|
||||
case cd_setting:
|
||||
quick_ticks_running = true;
|
||||
movement_request_tick_frequency(8);
|
||||
break;
|
||||
default:
|
||||
// Toggle auto-repeat
|
||||
button_beep(settings);
|
||||
state->repeat = !state->repeat;
|
||||
if(state->repeat)
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
else
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
|
@ -282,7 +322,7 @@ bool countdown_face_loop(movement_event_t event, movement_settings_t *settings,
|
|||
abort_quick_ticks(state);
|
||||
break;
|
||||
case EVENT_BACKGROUND_TASK:
|
||||
ring(state);
|
||||
times_up(settings, state);
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
abort_quick_ticks(state);
|
||||
|
|
|
@ -62,6 +62,8 @@ typedef struct {
|
|||
uint8_t set_seconds;
|
||||
uint8_t selection;
|
||||
countdown_mode_t mode;
|
||||
bool repeat;
|
||||
uint8_t watch_face_index;
|
||||
} countdown_state_t;
|
||||
|
||||
|
||||
|
|
|
@ -26,8 +26,7 @@
|
|||
#include <string.h>
|
||||
#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};
|
||||
#include "watch_utility.h"
|
||||
|
||||
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
|
||||
|
@ -66,13 +65,12 @@ static void _day_one_face_increment(day_one_state_t *state) {
|
|||
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;
|
||||
}
|
||||
if (state->birth_day == 0 || state->birth_day > days_in_month(state->birth_month, state->birth_year))
|
||||
state->birth_day = 1;
|
||||
}
|
||||
|
||||
void day_one_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
|
||||
|
|
649
movement/watch_faces/complication/deadline_face.c
Normal file
649
movement/watch_faces/complication/deadline_face.c
Normal file
|
@ -0,0 +1,649 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023-2024 Konrad Rieck
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* # Deadline Face
|
||||
*
|
||||
* This is a watch face for tracking deadlines. It draws inspiration from
|
||||
* other watch faces of the project but focuses on keeping track of
|
||||
* deadlines. You can enter and monitor up to four different deadlines by
|
||||
* providing their respective date and time. The face has two modes:
|
||||
* *running mode* and *settings mode*.
|
||||
*
|
||||
* ## Running Mode
|
||||
*
|
||||
* When the watch face is activated, it defaults to running mode. The top
|
||||
* right corner shows the current deadline number, and the main display
|
||||
* presents the time left until the deadline. The format of the display
|
||||
* varies depending on the remaining time.
|
||||
*
|
||||
* - When less than a day is left, the display shows the remaining hours,
|
||||
* minutes, and seconds in the form `HH:MM:SS`.
|
||||
*
|
||||
* - When less than a month is left, the display shows the remaining days
|
||||
* and hours in the form `DD:HH` with the unit `dy` for days.
|
||||
*
|
||||
* - When less than a year is left, the display shows the remaining months
|
||||
* and days in the form `MM:DD` with the unit `mo` for months.
|
||||
*
|
||||
* - When more than a year is left, the years and months are displayed in
|
||||
* the form `YY:MM` with the unit `yr` for years.
|
||||
*
|
||||
* - When a deadline has passed in the last 24 hours, the display shows
|
||||
* `over` to indicate that the deadline has just recently been reached.
|
||||
*
|
||||
* - When no deadline is set for a particular slot, or if a deadline has
|
||||
* already passed by more than 24 hours, `--:--` is displayed.
|
||||
*
|
||||
* The user can navigate in running mode using the following buttons:
|
||||
*
|
||||
* - The *alarm button* moves the next deadline. There are currently four
|
||||
* slots available for deadlines. When the last slot has been reached,
|
||||
* pressing the button moves to the first slot.
|
||||
*
|
||||
* - A *long press* on the *alarm button* activates settings mode and
|
||||
* enables configuring the currently selected deadline.
|
||||
*
|
||||
* - A *long press* on the *light button* activates a deadline alarm. The
|
||||
* bell icon is displayed, and the alarm will ring upon reaching any of
|
||||
* the deadlines set. It is important to note that the watch will not
|
||||
* enter low-energy sleep mode while the alarm is enabled.
|
||||
*
|
||||
*
|
||||
* ## Settings Mode
|
||||
*
|
||||
* In settings mode, the currently selected slot for a deadline can be
|
||||
* configured by providing the date and the time. Like running mode, the
|
||||
* top right corner of the display indicates the current deadline number.
|
||||
* The main display shows the date and, on the next page, the time to be
|
||||
* configured.
|
||||
*
|
||||
* The user can use the following buttons in settings mode.
|
||||
*
|
||||
* - The *light button* navigates through the different date and time
|
||||
* settings, going from year, month, day, hour, to minute. The selected
|
||||
* position is blinking.
|
||||
*
|
||||
* - A *long press* on the light button resets the date and time to the next
|
||||
* day at midnight. This is the default deadline.
|
||||
*
|
||||
* - The *alarm button* increments the currently selected position. A *long
|
||||
* press* on the *alarm button* changes the value faster.
|
||||
*
|
||||
* - The *mode button* exists setting mode and returns to *running mode*.
|
||||
* Here the selected deadline slot can be changed.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "deadline_face.h"
|
||||
#include "watch.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
#define SETTINGS_NUM (5)
|
||||
const char settings_titles[SETTINGS_NUM][3] = { "YR", "MO", "DA", "HR", "M1" };
|
||||
|
||||
/* Local functions */
|
||||
static void _running_init(movement_settings_t *settings, deadline_state_t *state);
|
||||
static bool _running_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
static void _running_display(movement_event_t event, movement_settings_t *settings, deadline_state_t *state);
|
||||
static void _setting_init(movement_settings_t *settings, deadline_state_t *state);
|
||||
static bool _setting_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
static void _setting_display(movement_event_t event, movement_settings_t *settings, deadline_state_t *state, watch_date_time date);
|
||||
|
||||
/* Utility functions */
|
||||
static void _background_alarm_play(movement_settings_t *settings, deadline_state_t *state);
|
||||
static void _background_alarm_schedule(movement_settings_t *settings, deadline_state_t *state);
|
||||
static void _background_alarm_cancel(movement_settings_t *settings, deadline_state_t *state);
|
||||
static void _increment_date(movement_settings_t *settings, deadline_state_t *state, watch_date_time date_time);
|
||||
static inline int32_t _get_tz_offset(movement_settings_t *settings);
|
||||
static inline void _change_tick_freq(uint8_t freq, deadline_state_t *state);
|
||||
static inline bool _is_leap(int16_t y);
|
||||
static inline int _days_in_month(int16_t mpnth, int16_t y);
|
||||
static inline unsigned int _mod(int a, int b);
|
||||
static inline void _beep_button(movement_settings_t *settings);
|
||||
static inline void _beep_enable(movement_settings_t *settings);
|
||||
static inline void _beep_disable(movement_settings_t *settings);
|
||||
static inline void _reset_deadline(movement_settings_t *settings, deadline_state_t *state);
|
||||
|
||||
/* Check for leap year */
|
||||
static inline bool _is_leap(int16_t y)
|
||||
{
|
||||
y += 1900;
|
||||
return !(y % 4) && ((y % 100) || !(y % 400));
|
||||
}
|
||||
|
||||
/* Modulo function */
|
||||
static inline unsigned int _mod(int a, int b)
|
||||
{
|
||||
int r = a % b;
|
||||
return r < 0 ? r + b : r;
|
||||
}
|
||||
|
||||
/* Return days in month */
|
||||
static inline int _days_in_month(int16_t month, int16_t year)
|
||||
{
|
||||
uint8_t days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
||||
month = _mod(month - 1, 12);
|
||||
|
||||
if (month == 1 && _is_leap(year)) {
|
||||
return days[month] + 1;
|
||||
} else {
|
||||
return days[month];
|
||||
}
|
||||
}
|
||||
|
||||
/* Return time zone offset */
|
||||
static inline int32_t _get_tz_offset(movement_settings_t *settings)
|
||||
{
|
||||
return movement_timezone_offsets[settings->bit.time_zone] * 60;
|
||||
}
|
||||
|
||||
/* Beep for a button press*/
|
||||
static inline void _beep_button(movement_settings_t *settings)
|
||||
{
|
||||
// Play a beep as confirmation for a button press (if applicable)
|
||||
if (!settings->bit.button_should_sound)
|
||||
return;
|
||||
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
|
||||
}
|
||||
|
||||
/* Beep for entering settings */
|
||||
static inline void _beep_enable(movement_settings_t *settings)
|
||||
{
|
||||
if (!settings->bit.button_should_sound)
|
||||
return;
|
||||
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G7, 50);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_REST, 75);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C8, 75);
|
||||
}
|
||||
|
||||
/* Beep for leaving settings */
|
||||
static inline void _beep_disable(movement_settings_t *settings)
|
||||
{
|
||||
if (!settings->bit.button_should_sound)
|
||||
return;
|
||||
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C8, 50);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_REST, 75);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G7, 75);
|
||||
}
|
||||
|
||||
/* Change tick frequency */
|
||||
static inline void _change_tick_freq(uint8_t freq, deadline_state_t *state)
|
||||
{
|
||||
if (state->tick_freq != freq) {
|
||||
movement_request_tick_frequency(freq);
|
||||
state->tick_freq = freq;
|
||||
}
|
||||
}
|
||||
|
||||
/* Determine index of closest deadline */
|
||||
static uint8_t _closest_deadline(movement_settings_t *settings, deadline_state_t *state)
|
||||
{
|
||||
watch_date_time now = watch_rtc_get_date_time();
|
||||
uint32_t now_ts = watch_utility_date_time_to_unix_time(now, _get_tz_offset(settings));
|
||||
uint32_t min_ts = UINT32_MAX;
|
||||
uint8_t min_index = 0;
|
||||
|
||||
for (uint8_t i = 0; i < DEADLINE_FACE_DATES; i++) {
|
||||
if (state->deadlines[i] < now_ts || state->deadlines[i] > min_ts)
|
||||
continue;
|
||||
min_ts = state->deadlines[i];
|
||||
min_index = i;
|
||||
}
|
||||
|
||||
return min_index;
|
||||
}
|
||||
|
||||
/* Play background alarm */
|
||||
static void _background_alarm_play(movement_settings_t *settings, deadline_state_t *state)
|
||||
{
|
||||
(void) settings;
|
||||
|
||||
/* Use the default alarm from movement and move to foreground */
|
||||
if (state->alarm_enabled) {
|
||||
movement_play_alarm();
|
||||
movement_move_to_face(state->face_idx);
|
||||
}
|
||||
}
|
||||
|
||||
/* Schedule background alarm */
|
||||
static void _background_alarm_schedule(movement_settings_t *settings, deadline_state_t *state)
|
||||
{
|
||||
/* We simply re-use the scheduling in the background task */
|
||||
deadline_face_wants_background_task(settings, state);
|
||||
}
|
||||
|
||||
/* Cancel background alarm */
|
||||
static void _background_alarm_cancel(movement_settings_t *settings, deadline_state_t *state)
|
||||
{
|
||||
(void) settings;
|
||||
|
||||
movement_cancel_background_task_for_face(state->face_idx);
|
||||
}
|
||||
|
||||
/* Reset deadline to tomorrow */
|
||||
static inline void _reset_deadline(movement_settings_t *settings, deadline_state_t *state)
|
||||
{
|
||||
/* Get current time and reset hours/minutes/seconds */
|
||||
watch_date_time date_time = watch_rtc_get_date_time();
|
||||
date_time.unit.second = 0;
|
||||
date_time.unit.minute = 0;
|
||||
date_time.unit.hour = 0;
|
||||
|
||||
/* Add 24 hours to obtain first second of tomorrow */
|
||||
uint32_t ts = watch_utility_date_time_to_unix_time(date_time, _get_tz_offset(settings));
|
||||
ts += 24 * 60 * 60;
|
||||
|
||||
state->deadlines[state->current_index] = ts;
|
||||
}
|
||||
|
||||
/* Increment date in settings mode. Function taken from `set_time_face.c` */
|
||||
static void _increment_date(movement_settings_t *settings, deadline_state_t *state, watch_date_time date_time)
|
||||
{
|
||||
const uint8_t days_in_month[12] = { 31, 28, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31 };
|
||||
|
||||
switch (state->current_page) {
|
||||
case 0:
|
||||
/* Only 10 years covered. Fix this sometime next decade */
|
||||
date_time.unit.year = ((date_time.unit.year % 10) + 1);
|
||||
break;
|
||||
case 1:
|
||||
date_time.unit.month = (date_time.unit.month % 12) + 1;
|
||||
break;
|
||||
case 2:
|
||||
date_time.unit.day = date_time.unit.day + 1;
|
||||
|
||||
/* Check for leap years */
|
||||
int8_t days = days_in_month[date_time.unit.month - 1];
|
||||
if (date_time.unit.month == 2 && _is_leap(date_time.unit.year))
|
||||
days++;
|
||||
|
||||
if (date_time.unit.day > days)
|
||||
date_time.unit.day = 1;
|
||||
break;
|
||||
case 3:
|
||||
date_time.unit.hour = (date_time.unit.hour + 1) % 24;
|
||||
break;
|
||||
case 4:
|
||||
date_time.unit.minute = (date_time.unit.minute + 1) % 60;
|
||||
break;
|
||||
}
|
||||
|
||||
uint32_t ts = watch_utility_date_time_to_unix_time(date_time, _get_tz_offset(settings));
|
||||
state->deadlines[state->current_index] = ts;
|
||||
}
|
||||
|
||||
/* Update display in running mode */
|
||||
static void _running_display(movement_event_t event, movement_settings_t *settings, deadline_state_t *state)
|
||||
{
|
||||
(void) event;
|
||||
(void) settings;
|
||||
|
||||
/* Seconds, minutes, hours, days, months, years */
|
||||
int16_t unit[] = { 0, 0, 0, 0, 0, 0 };
|
||||
uint8_t i, range[] = { 60, 60, 24, 30, 12, 0 };
|
||||
char buf[16];
|
||||
|
||||
/* Display indicators */
|
||||
if (state->alarm_enabled)
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
else
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
|
||||
watch_date_time now = watch_rtc_get_date_time();
|
||||
uint32_t now_ts = watch_utility_date_time_to_unix_time(now, _get_tz_offset(settings));
|
||||
|
||||
/* Deadline expired */
|
||||
if (state->deadlines[state->current_index] < now_ts) {
|
||||
if (state->deadlines[state->current_index] + 24 * 60 * 60 > now_ts)
|
||||
sprintf(buf, "DL%2dOVER ", state->current_index + 1);
|
||||
else
|
||||
sprintf(buf, "DL%2d---- ", state->current_index + 1);
|
||||
|
||||
//watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
watch_display_string(buf, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Get date time structs */
|
||||
watch_date_time deadline = watch_utility_date_time_from_unix_time(state->deadlines[state->current_index], _get_tz_offset(settings)
|
||||
);
|
||||
|
||||
/* Calculate naive difference of dates */
|
||||
unit[0] = deadline.unit.second - now.unit.second;
|
||||
unit[1] = deadline.unit.minute - now.unit.minute;
|
||||
unit[2] = deadline.unit.hour - now.unit.hour;
|
||||
unit[3] = deadline.unit.day - now.unit.day;
|
||||
unit[4] = deadline.unit.month - now.unit.month;
|
||||
unit[5] = deadline.unit.year - now.unit.year;
|
||||
|
||||
/* Correct errors of naive difference */
|
||||
for (i = 0; i < 6; i++) {
|
||||
if (unit[i] < 0) {
|
||||
/* Correct remaining units */
|
||||
if (i == 3)
|
||||
unit[i] += _days_in_month(deadline.unit.month - 1, deadline.unit.year);
|
||||
else
|
||||
unit[i] += range[i];
|
||||
|
||||
/* Carry over change to next unit if non-zero */
|
||||
if (i < 5 && unit[i + 1] != 0)
|
||||
unit[i + 1] -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set range */
|
||||
i = state->current_index + 1;
|
||||
if (unit[5] > 0) {
|
||||
/* years:months */
|
||||
sprintf(buf, "DL%2d%02d%02dYR", i, unit[5] % 100, unit[4] % 12);
|
||||
} else if (unit[4] > 0) {
|
||||
/* months:days */
|
||||
sprintf(buf, "DL%2d%02d%02dMO", i, (unit[5] * 12 + unit[4]) % 100, unit[3] % 32);
|
||||
} else if (unit[3] > 0) {
|
||||
/* days:hours */
|
||||
sprintf(buf, "DL%2d%02d%02ddY", i, unit[3] % 32, unit[2] % 24);
|
||||
} else {
|
||||
/* hours:minutes:seconds */
|
||||
sprintf(buf, "DL%2d%02d%02d%02d", i, unit[2] % 24, unit[1] % 60, unit[0] % 60);
|
||||
}
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
/* Init running mode */
|
||||
static void _running_init(movement_settings_t *settings, deadline_state_t *state)
|
||||
{
|
||||
(void) settings;
|
||||
(void) state;
|
||||
|
||||
watch_clear_indicator(WATCH_INDICATOR_24H);
|
||||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
watch_set_colon();
|
||||
|
||||
/* Ensure 1Hz updates only */
|
||||
_change_tick_freq(1, state);
|
||||
}
|
||||
|
||||
/* Loop of running mode */
|
||||
static bool _running_loop(movement_event_t event, movement_settings_t *settings, void *context)
|
||||
{
|
||||
deadline_state_t *state = (deadline_state_t *) context;
|
||||
|
||||
if (event.event_type != EVENT_BACKGROUND_TASK)
|
||||
_running_display(event, settings, state);
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
_beep_button(settings);
|
||||
state->current_index = (state->current_index + 1) % DEADLINE_FACE_DATES;
|
||||
_running_display(event, settings, state);
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
_beep_enable(settings);
|
||||
_setting_init(settings, state);
|
||||
state->mode = DEADLINE_FACE_SETTING;
|
||||
break;
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
movement_move_to_next_face();
|
||||
return false;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
_beep_button(settings);
|
||||
state->alarm_enabled = !state->alarm_enabled;
|
||||
if (state->alarm_enabled) {
|
||||
_background_alarm_schedule(settings, context);
|
||||
} else {
|
||||
_background_alarm_cancel(settings, context);
|
||||
}
|
||||
_running_display(event, settings, state);
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
movement_move_to_face(0);
|
||||
break;
|
||||
case EVENT_BACKGROUND_TASK:
|
||||
_background_alarm_play(settings, state);
|
||||
break;
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Update display in settings mode */
|
||||
static void _setting_display(movement_event_t event, movement_settings_t *settings, deadline_state_t *state, watch_date_time date_time)
|
||||
{
|
||||
char buf[11];
|
||||
|
||||
int i = state->current_index + 1;
|
||||
if (state->current_page > 2) {
|
||||
watch_set_colon();
|
||||
if (settings->bit.clock_mode_24h) {
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
sprintf(buf, "%s%2d%2d%02d ", settings_titles[state->current_page], i, date_time.unit.hour, date_time.unit.minute);
|
||||
} else {
|
||||
sprintf(buf, "%s%2d%2d%02d ", settings_titles[state->current_page], i, (date_time.unit.hour % 12) ? (date_time.unit.hour % 12) : 12,
|
||||
date_time.unit.minute);
|
||||
if (date_time.unit.hour < 12)
|
||||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
else
|
||||
watch_set_indicator(WATCH_INDICATOR_PM);
|
||||
}
|
||||
} else {
|
||||
watch_clear_colon();
|
||||
watch_clear_indicator(WATCH_INDICATOR_24H);
|
||||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
sprintf(buf, "%s%2d%2d%02d%02d", settings_titles[state->current_page], i, date_time.unit.year + 20, date_time.unit.month, date_time.unit.day);
|
||||
}
|
||||
|
||||
/* Blink up the parameter we are setting */
|
||||
if (event.subsecond % 2) {
|
||||
switch (state->current_page) {
|
||||
case 0:
|
||||
case 3:
|
||||
buf[4] = buf[5] = ' ';
|
||||
break;
|
||||
case 1:
|
||||
case 4:
|
||||
buf[6] = buf[7] = ' ';
|
||||
break;
|
||||
case 2:
|
||||
buf[8] = buf[9] = ' ';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
/* Init setting mode */
|
||||
static void _setting_init(movement_settings_t *settings, deadline_state_t *state)
|
||||
{
|
||||
state->current_page = 0;
|
||||
|
||||
/* Init fresh deadline to next day */
|
||||
if (state->deadlines[state->current_index] == 0) {
|
||||
_reset_deadline(settings, state);
|
||||
}
|
||||
|
||||
/* Ensure 1Hz updates only */
|
||||
_change_tick_freq(1, state);
|
||||
}
|
||||
|
||||
/* Loop of setting mode */
|
||||
static bool _setting_loop(movement_event_t event, movement_settings_t *settings, void *context)
|
||||
{
|
||||
deadline_state_t *state = (deadline_state_t *) context;
|
||||
watch_date_time date_time;
|
||||
date_time = watch_utility_date_time_from_unix_time(state->deadlines[state->current_index], _get_tz_offset(settings));
|
||||
|
||||
if (event.event_type != EVENT_BACKGROUND_TASK)
|
||||
_setting_display(event, settings, state, date_time);
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_TICK:
|
||||
if (state->tick_freq == 8) {
|
||||
if (watch_get_pin_level(BTN_ALARM)) {
|
||||
_increment_date(settings, state, date_time);
|
||||
_setting_display(event, settings, state, date_time);
|
||||
} else {
|
||||
_change_tick_freq(4, state);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
_change_tick_freq(8, state);
|
||||
break;
|
||||
case EVENT_ALARM_LONG_UP:
|
||||
_change_tick_freq(4, state);
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
_beep_button(settings);
|
||||
_reset_deadline(settings, state);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
state->current_page = (state->current_page + 1) % SETTINGS_NUM;
|
||||
_setting_display(event, settings, state, date_time);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
_change_tick_freq(4, state);
|
||||
_increment_date(settings, state, date_time);
|
||||
_setting_display(event, settings, state, date_time);
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
_beep_button(settings);
|
||||
_background_alarm_schedule(settings, context);
|
||||
_change_tick_freq(1, state);
|
||||
state->mode = DEADLINE_FACE_RUNNING;
|
||||
movement_move_to_face(0);
|
||||
break;
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
_beep_disable(settings);
|
||||
_background_alarm_schedule(settings, context);
|
||||
_running_init(settings, state);
|
||||
_running_display(event, settings, state);
|
||||
state->mode = DEADLINE_FACE_RUNNING;
|
||||
break;
|
||||
case EVENT_BACKGROUND_TASK:
|
||||
_background_alarm_play(settings, state);
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Setup face */
|
||||
void deadline_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr)
|
||||
{
|
||||
(void) settings;
|
||||
(void) watch_face_index;
|
||||
if (*context_ptr != NULL)
|
||||
return; /* Skip setup if context available */
|
||||
|
||||
/* Allocate state */
|
||||
*context_ptr = malloc(sizeof(deadline_state_t));
|
||||
memset(*context_ptr, 0, sizeof(deadline_state_t));
|
||||
|
||||
/* Store face index for background tasks */
|
||||
deadline_state_t *state = (deadline_state_t *) *context_ptr;
|
||||
state->face_idx = watch_face_index;
|
||||
}
|
||||
|
||||
/* Activate face */
|
||||
void deadline_face_activate(movement_settings_t *settings, void *context)
|
||||
{
|
||||
(void) settings;
|
||||
deadline_state_t *state = (deadline_state_t *) context;
|
||||
|
||||
/* Set display options */
|
||||
_running_init(settings, state);
|
||||
state->mode = DEADLINE_FACE_RUNNING;
|
||||
state->current_index = _closest_deadline(settings, state);
|
||||
}
|
||||
|
||||
/* Loop face */
|
||||
bool deadline_face_loop(movement_event_t event, movement_settings_t *settings, void *context)
|
||||
{
|
||||
(void) settings;
|
||||
deadline_state_t *state = (deadline_state_t *) context;
|
||||
switch (state->mode) {
|
||||
case DEADLINE_FACE_SETTING:
|
||||
_setting_loop(event, settings, context);
|
||||
break;
|
||||
default:
|
||||
case DEADLINE_FACE_RUNNING:
|
||||
_running_loop(event, settings, context);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Resign face */
|
||||
void deadline_face_resign(movement_settings_t *settings, void *context)
|
||||
{
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
/* Want background task */
|
||||
bool deadline_face_wants_background_task(movement_settings_t *settings, void *context)
|
||||
{
|
||||
deadline_state_t *state = (deadline_state_t *) context;
|
||||
|
||||
if (!state->alarm_enabled)
|
||||
return false;
|
||||
|
||||
/* Determine closest deadline */
|
||||
watch_date_time now = watch_rtc_get_date_time();
|
||||
uint32_t now_ts = watch_utility_date_time_to_unix_time(now, _get_tz_offset(settings));
|
||||
uint32_t next_ts = state->deadlines[_closest_deadline(settings, state)];
|
||||
|
||||
/* No active deadline */
|
||||
if (next_ts < now_ts)
|
||||
return false;
|
||||
|
||||
/* No deadline within next 60 seconds */
|
||||
if (next_ts >= now_ts + 60)
|
||||
return false;
|
||||
|
||||
/* Deadline within next minute. Let's set up an alarm */
|
||||
watch_date_time next = watch_utility_date_time_from_unix_time(next_ts, _get_tz_offset(settings));
|
||||
movement_request_wake();
|
||||
movement_schedule_background_task_for_face(state->face_idx, next);
|
||||
return false;
|
||||
}
|
65
movement/watch_faces/complication/deadline_face.h
Normal file
65
movement/watch_faces/complication/deadline_face.h
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023-2024 Konrad Rieck
|
||||
*
|
||||
* 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 DEADLINE_FACE_H_
|
||||
#define DEADLINE_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/* Modes of face */
|
||||
typedef enum {
|
||||
DEADLINE_FACE_RUNNING = 0,
|
||||
DEADLINE_FACE_SETTING
|
||||
} deadline_mode_t;
|
||||
|
||||
/* Number of deadline dates */
|
||||
#define DEADLINE_FACE_DATES (4)
|
||||
|
||||
/* Deadline configuration */
|
||||
typedef struct {
|
||||
deadline_mode_t mode:1;
|
||||
uint8_t current_page:3;
|
||||
uint8_t current_index:2;
|
||||
uint8_t alarm_enabled:1;
|
||||
uint8_t tick_freq;
|
||||
uint8_t face_idx;
|
||||
uint32_t deadlines[DEADLINE_FACE_DATES];
|
||||
} deadline_state_t;
|
||||
|
||||
void deadline_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr);
|
||||
void deadline_face_activate(movement_settings_t *settings, void *context);
|
||||
bool deadline_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void deadline_face_resign(movement_settings_t *settings, void *context);
|
||||
bool deadline_face_wants_background_task(movement_settings_t *settings, void *context);
|
||||
|
||||
#define deadline_face ((const watch_face_t){ \
|
||||
deadline_face_setup, \
|
||||
deadline_face_activate, \
|
||||
deadline_face_loop, \
|
||||
deadline_face_resign, \
|
||||
deadline_face_wants_background_task \
|
||||
})
|
||||
|
||||
#endif // DEADLINE_FACE_H_
|
617
movement/watch_faces/complication/endless_runner_face.c
Normal file
617
movement/watch_faces/complication/endless_runner_face.c
Normal file
|
@ -0,0 +1,617 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 <David Volovskiy>
|
||||
*
|
||||
* 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 "endless_runner_face.h"
|
||||
|
||||
typedef enum {
|
||||
JUMPING_FINAL_FRAME = 0,
|
||||
NOT_JUMPING,
|
||||
JUMPING_START,
|
||||
} RunnerJumpState;
|
||||
|
||||
typedef enum {
|
||||
SCREEN_TITLE = 0,
|
||||
SCREEN_PLAYING,
|
||||
SCREEN_LOSE,
|
||||
SCREEN_TIME,
|
||||
SCREEN_COUNT
|
||||
} RunnerCurrScreen;
|
||||
|
||||
typedef enum {
|
||||
DIFF_BABY = 0, // FREQ_SLOW FPS; MIN_ZEROES 0's min; Jump is JUMP_FRAMES_EASY frames
|
||||
DIFF_EASY, // FREQ FPS; MIN_ZEROES 0's min; Jump is JUMP_FRAMES_EASY frames
|
||||
DIFF_NORM, // FREQ FPS; MIN_ZEROES 0's min; Jump is JUMP_FRAMES frames
|
||||
DIFF_HARD, // FREQ FPS; MIN_ZEROES_HARD 0's min; jump is JUMP_FRAMES frames
|
||||
DIFF_FUEL, // Mode where the top-right displays the amoount of fuel that you can be above the ground for, dodging obstacles. When on the ground, your fuel recharges.
|
||||
DIFF_FUEL_1, // Same as DIFF_FUEL, but if your fuel is 0, then you won't recharge
|
||||
DIFF_COUNT
|
||||
} RunnerDifficulty;
|
||||
|
||||
#define NUM_GRID 12 // This the length that the obstacle track can be on
|
||||
#define FREQ 8 // Frequency request for the game
|
||||
#define FREQ_SLOW 4 // Frequency request for baby mode
|
||||
#define JUMP_FRAMES 2 // Wait this many frames on difficulties above EASY before coming down from the jump button pressed
|
||||
#define JUMP_FRAMES_EASY 3 // Wait this many frames on difficulties at or below EASY before coming down from the jump button pressed
|
||||
#define MIN_ZEROES 4 // At minimum, we'll have this many spaces between obstacles
|
||||
#define MIN_ZEROES_HARD 3 // At minimum, we'll have this many spaces between obstacles on hard mode
|
||||
#define MAX_HI_SCORE 9999 // Max hi score to store and display on the title screen.
|
||||
#define MAX_DISP_SCORE 39 // The top-right digits can't properly display above 39
|
||||
#define JUMP_FRAMES_FUEL 30 // The max fuel that fuel that the fuel mode game will hold
|
||||
#define JUMP_FRAMES_FUEL_RECHARGE 3 // How much fuel each frame on the ground adds
|
||||
#define MAX_DISP_SCORE_FUEL 9 // Since the fuel mode displays the score in the weekday slot, two digits will display wonky data
|
||||
|
||||
typedef struct {
|
||||
uint32_t obst_pattern;
|
||||
uint16_t obst_indx : 8;
|
||||
uint16_t jump_state : 5;
|
||||
uint16_t sec_before_moves : 3;
|
||||
uint16_t curr_score : 10;
|
||||
uint16_t curr_screen : 4;
|
||||
bool loc_2_on;
|
||||
bool loc_3_on;
|
||||
bool success_jump;
|
||||
bool fuel_mode;
|
||||
uint8_t fuel;
|
||||
} game_state_t;
|
||||
|
||||
static game_state_t game_state;
|
||||
static const uint8_t _num_bits_obst_pattern = sizeof(game_state.obst_pattern) * 8;
|
||||
|
||||
static void print_binary(uint32_t value, int bits) {
|
||||
#if __EMSCRIPTEN__
|
||||
for (int i = bits - 1; i >= 0; i--) {
|
||||
// Print each bit
|
||||
printf("%lu", (value >> i) & 1);
|
||||
// Optional: add a space every 4 bits for readability
|
||||
if (i % 4 == 0 && i != 0) {
|
||||
printf(" ");
|
||||
}
|
||||
}
|
||||
printf("\r\n");
|
||||
#else
|
||||
(void) value;
|
||||
(void) bits;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
static uint32_t get_random(uint32_t max) {
|
||||
#if __EMSCRIPTEN__
|
||||
return rand() % max;
|
||||
#else
|
||||
return arc4random_uniform(max);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint32_t get_random_nonzero(uint32_t max) {
|
||||
uint32_t random;
|
||||
do
|
||||
{
|
||||
random = get_random(max);
|
||||
} while (random == 0);
|
||||
return random;
|
||||
}
|
||||
|
||||
static uint32_t get_random_kinda_nonzero(uint32_t max) {
|
||||
// Returns a number that's between 1 and max, unless max is 0 or 1, then it returns 0 to max.
|
||||
if (max == 0) return 0;
|
||||
else if (max == 1) return get_random(max);
|
||||
return get_random_nonzero(max);
|
||||
}
|
||||
|
||||
static uint32_t get_random_fuel(uint32_t prev_val) {
|
||||
static uint8_t prev_rand_subset = 0;
|
||||
uint32_t rand;
|
||||
uint8_t max_ones, subset;
|
||||
uint32_t rand_legal = 0;
|
||||
prev_val = prev_val & ~0xFFFF;
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
subset = 0;
|
||||
max_ones = 8;
|
||||
if (prev_rand_subset > 4)
|
||||
max_ones -= prev_rand_subset;
|
||||
rand = get_random_kinda_nonzero(max_ones);
|
||||
if (rand > 5 && prev_rand_subset) rand = 5; // The gap of one or two is awkward
|
||||
for (uint32_t j = 0; j < rand; j++) {
|
||||
subset |= (1 << j);
|
||||
}
|
||||
if (prev_rand_subset >= 7)
|
||||
subset = subset << 1;
|
||||
subset &= 0xFF;
|
||||
rand_legal |= subset << (8 * i);
|
||||
prev_rand_subset = rand;
|
||||
}
|
||||
|
||||
rand_legal = prev_val | rand_legal;
|
||||
print_binary(rand_legal, 32);
|
||||
return rand_legal;
|
||||
}
|
||||
|
||||
static uint32_t get_random_legal(uint32_t prev_val, uint16_t difficulty) {
|
||||
/** @brief A legal random number starts with the previous number (which should be the 12 bits on the screen).
|
||||
* @param prev_val The previous value to tack onto. The return will have its first NUM_GRID MSBs be the same as prev_val, and the rest be new
|
||||
* @param difficulty To dictate how spread apart the obsticles must be
|
||||
* @return the new random value, where it's first NUM_GRID MSBs are the same as prev_val
|
||||
*/
|
||||
uint8_t min_zeros = (difficulty == DIFF_HARD) ? MIN_ZEROES_HARD : MIN_ZEROES;
|
||||
uint32_t max = (1 << (_num_bits_obst_pattern - NUM_GRID)) - 1;
|
||||
uint32_t rand = get_random_nonzero(max);
|
||||
uint32_t rand_legal = 0;
|
||||
prev_val = prev_val & ~max;
|
||||
|
||||
for (int i = (NUM_GRID + 1); i <= _num_bits_obst_pattern; i++) {
|
||||
uint32_t mask = 1 << (_num_bits_obst_pattern - i);
|
||||
bool msb = (rand & mask) >> (_num_bits_obst_pattern - i);
|
||||
if (msb) {
|
||||
rand_legal = rand_legal << min_zeros;
|
||||
i+=min_zeros;
|
||||
}
|
||||
rand_legal |= msb;
|
||||
rand_legal = rand_legal << 1;
|
||||
}
|
||||
|
||||
rand_legal = rand_legal & max;
|
||||
for (int i = 0; i <= min_zeros; i++) {
|
||||
if (prev_val & (1 << (i + _num_bits_obst_pattern - NUM_GRID))){
|
||||
rand_legal = rand_legal >> (min_zeros - i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
rand_legal = prev_val | rand_legal;
|
||||
print_binary(rand_legal, 32);
|
||||
return rand_legal;
|
||||
}
|
||||
|
||||
static void display_ball(bool jumping) {
|
||||
if (!jumping) {
|
||||
watch_set_pixel(0, 21);
|
||||
watch_set_pixel(1, 21);
|
||||
watch_set_pixel(0, 20);
|
||||
watch_set_pixel(1, 20);
|
||||
watch_clear_pixel(1, 17);
|
||||
watch_clear_pixel(2, 20);
|
||||
watch_clear_pixel(2, 21);
|
||||
}
|
||||
else {
|
||||
watch_clear_pixel(0, 21);
|
||||
watch_clear_pixel(1, 21);
|
||||
watch_clear_pixel(0, 20);
|
||||
watch_set_pixel(1, 20);
|
||||
watch_set_pixel(1, 17);
|
||||
watch_set_pixel(2, 20);
|
||||
watch_set_pixel(2, 21);
|
||||
}
|
||||
}
|
||||
|
||||
static void display_score(uint8_t score) {
|
||||
char buf[3];
|
||||
if (game_state.fuel_mode) {
|
||||
score %= (MAX_DISP_SCORE_FUEL + 1);
|
||||
sprintf(buf, "%1d", score);
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
else {
|
||||
score %= (MAX_DISP_SCORE + 1);
|
||||
sprintf(buf, "%2d", score);
|
||||
watch_display_string(buf, 2);
|
||||
}
|
||||
}
|
||||
|
||||
static void add_to_score(endless_runner_state_t *state) {
|
||||
if (game_state.curr_score <= MAX_HI_SCORE) {
|
||||
game_state.curr_score++;
|
||||
if (game_state.curr_score > state -> hi_score)
|
||||
state -> hi_score = game_state.curr_score;
|
||||
}
|
||||
game_state.success_jump = true;
|
||||
display_score(game_state.curr_score);
|
||||
}
|
||||
|
||||
static void display_fuel(uint8_t subsecond, uint8_t difficulty) {
|
||||
char buf[4];
|
||||
if (difficulty == DIFF_FUEL_1 && game_state.fuel == 0 && subsecond % (FREQ/2) == 0) {
|
||||
watch_display_string(" ", 2); // Blink the 0 fuel to show it cannot be refilled.
|
||||
return;
|
||||
}
|
||||
sprintf(buf, "%2d", game_state.fuel);
|
||||
watch_display_string(buf, 2);
|
||||
}
|
||||
|
||||
static void check_and_reset_hi_score(endless_runner_state_t *state) {
|
||||
// Resets the hi score at the beginning of each month.
|
||||
watch_date_time date_time = watch_rtc_get_date_time();
|
||||
if ((state -> year_last_hi_score != date_time.unit.year) ||
|
||||
(state -> month_last_hi_score != date_time.unit.month))
|
||||
{
|
||||
// The high score resets itself every new month.
|
||||
state -> hi_score = 0;
|
||||
state -> year_last_hi_score = date_time.unit.year;
|
||||
state -> month_last_hi_score = date_time.unit.month;
|
||||
}
|
||||
}
|
||||
|
||||
static void display_difficulty(uint16_t difficulty) {
|
||||
switch (difficulty)
|
||||
{
|
||||
case DIFF_BABY:
|
||||
watch_display_string(" b", 2);
|
||||
break;
|
||||
case DIFF_EASY:
|
||||
watch_display_string(" E", 2);
|
||||
break;
|
||||
case DIFF_HARD:
|
||||
watch_display_string(" H", 2);
|
||||
break;
|
||||
case DIFF_FUEL:
|
||||
watch_display_string(" F", 2);
|
||||
break;
|
||||
case DIFF_FUEL_1:
|
||||
watch_display_string("1F", 2);
|
||||
break;
|
||||
case DIFF_NORM:
|
||||
default:
|
||||
watch_display_string(" N", 2);
|
||||
break;
|
||||
}
|
||||
game_state.fuel_mode = difficulty >= DIFF_FUEL && difficulty <= DIFF_FUEL_1;
|
||||
}
|
||||
|
||||
static void change_difficulty(endless_runner_state_t *state) {
|
||||
state -> difficulty = (state -> difficulty + 1) % DIFF_COUNT;
|
||||
display_difficulty(state -> difficulty);
|
||||
if (state -> soundOn) {
|
||||
if (state -> difficulty == 0) watch_buzzer_play_note(BUZZER_NOTE_B4, 30);
|
||||
else watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
|
||||
}
|
||||
}
|
||||
|
||||
static void toggle_sound(endless_runner_state_t *state) {
|
||||
state -> soundOn = !state -> soundOn;
|
||||
if (state -> soundOn){
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
}
|
||||
else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
}
|
||||
}
|
||||
|
||||
static void display_title(endless_runner_state_t *state) {
|
||||
uint16_t hi_score = state -> hi_score;
|
||||
uint8_t difficulty = state -> difficulty;
|
||||
bool sound_on = state -> soundOn;
|
||||
game_state.curr_screen = SCREEN_TITLE;
|
||||
memset(&game_state, 0, sizeof(game_state));
|
||||
game_state.sec_before_moves = 1; // The first obstacles will all be 0s, which is about an extra second of delay.
|
||||
if (sound_on) game_state.sec_before_moves--; // Start chime is about 1 second
|
||||
watch_set_colon();
|
||||
if (hi_score > MAX_HI_SCORE) {
|
||||
watch_display_string("ER HS --", 0);
|
||||
}
|
||||
else {
|
||||
char buf[14];
|
||||
sprintf(buf, "ER HS%4d", hi_score);
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
display_difficulty(difficulty);
|
||||
}
|
||||
|
||||
static void display_time(watch_date_time date_time, bool clock_mode_24h) {
|
||||
static watch_date_time previous_date_time;
|
||||
char buf[6 + 1];
|
||||
|
||||
// If the hour needs updating or it's the first time displaying the time
|
||||
if ((game_state.curr_screen != SCREEN_TIME) || (date_time.unit.hour != previous_date_time.unit.hour)) {
|
||||
uint8_t hour = date_time.unit.hour;
|
||||
game_state.curr_screen = SCREEN_TIME;
|
||||
|
||||
if (clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
else {
|
||||
if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM);
|
||||
hour %= 12;
|
||||
if (hour == 0) hour = 12;
|
||||
}
|
||||
watch_set_colon();
|
||||
sprintf( buf, "%2d%02d ", hour, date_time.unit.minute);
|
||||
watch_display_string(buf, 4);
|
||||
}
|
||||
// If both digits of the minute need updating
|
||||
else if ((date_time.unit.minute / 10) != (previous_date_time.unit.minute / 10)) {
|
||||
sprintf( buf, "%02d ", date_time.unit.minute);
|
||||
watch_display_string(buf, 6);
|
||||
}
|
||||
// If only the ones-place of the minute needs updating.
|
||||
else if (date_time.unit.minute != previous_date_time.unit.minute) {
|
||||
sprintf( buf, "%d ", date_time.unit.minute % 10);
|
||||
watch_display_string(buf, 7);
|
||||
}
|
||||
previous_date_time.reg = date_time.reg;
|
||||
}
|
||||
|
||||
static void begin_playing(endless_runner_state_t *state) {
|
||||
uint8_t difficulty = state -> difficulty;
|
||||
game_state.curr_screen = SCREEN_PLAYING;
|
||||
watch_clear_colon();
|
||||
movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ);
|
||||
if (game_state.fuel_mode) {
|
||||
watch_display_string(" ", 0);
|
||||
game_state.obst_pattern = get_random_fuel(0);
|
||||
if ((16 * JUMP_FRAMES_FUEL_RECHARGE) < JUMP_FRAMES_FUEL) // 16 frames of zeros at the start of a level
|
||||
game_state.fuel = JUMP_FRAMES_FUEL - (16 * JUMP_FRAMES_FUEL_RECHARGE); // Have it below its max to show it counting up when starting.
|
||||
if (game_state.fuel < JUMP_FRAMES_FUEL_RECHARGE) game_state.fuel = JUMP_FRAMES_FUEL_RECHARGE;
|
||||
}
|
||||
else {
|
||||
watch_display_string(" ", 2);
|
||||
game_state.obst_pattern = get_random_legal(0, difficulty);
|
||||
}
|
||||
game_state.jump_state = NOT_JUMPING;
|
||||
display_ball(game_state.jump_state != NOT_JUMPING);
|
||||
display_score( game_state.curr_score);
|
||||
if (state -> soundOn){
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C5, 200);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E5, 200);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G5, 200);
|
||||
}
|
||||
}
|
||||
|
||||
static void display_lose_screen(endless_runner_state_t *state) {
|
||||
game_state.curr_screen = SCREEN_LOSE;
|
||||
game_state.curr_score = 0;
|
||||
watch_display_string(" LOSE ", 0);
|
||||
if (state -> soundOn)
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A1, 600);
|
||||
else
|
||||
delay_ms(600);
|
||||
}
|
||||
|
||||
static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t *state) {
|
||||
static bool prev_obst_pos_two = 0;
|
||||
switch (grid_loc)
|
||||
{
|
||||
case 2:
|
||||
game_state.loc_2_on = obstacle;
|
||||
if (obstacle)
|
||||
watch_set_pixel(0, 20);
|
||||
else if (game_state.jump_state != NOT_JUMPING) {
|
||||
watch_clear_pixel(0, 20);
|
||||
if (game_state.fuel_mode && prev_obst_pos_two)
|
||||
add_to_score(state);
|
||||
}
|
||||
prev_obst_pos_two = obstacle;
|
||||
break;
|
||||
case 3:
|
||||
game_state.loc_3_on = obstacle;
|
||||
if (obstacle)
|
||||
watch_set_pixel(1, 21);
|
||||
else if (game_state.jump_state != NOT_JUMPING)
|
||||
watch_clear_pixel(1, 21);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
if (!game_state.fuel_mode && obstacle) // If an obstacle is here, it means the ball cleared it
|
||||
add_to_score(state);
|
||||
//fall through
|
||||
case 0:
|
||||
case 5:
|
||||
if (obstacle)
|
||||
watch_set_pixel(0, 18 + grid_loc);
|
||||
else
|
||||
watch_clear_pixel(0, 18 + grid_loc);
|
||||
break;
|
||||
case 4:
|
||||
if (obstacle)
|
||||
watch_set_pixel(1, 22);
|
||||
else
|
||||
watch_clear_pixel(1, 22);
|
||||
break;
|
||||
case 6:
|
||||
if (obstacle)
|
||||
watch_set_pixel(1, 0);
|
||||
else
|
||||
watch_clear_pixel(1, 0);
|
||||
break;
|
||||
case 7:
|
||||
case 8:
|
||||
if (obstacle)
|
||||
watch_set_pixel(0, grid_loc - 6);
|
||||
else
|
||||
watch_clear_pixel(0, grid_loc - 6);
|
||||
break;
|
||||
case 9:
|
||||
case 10:
|
||||
if (obstacle)
|
||||
watch_set_pixel(0, grid_loc - 5);
|
||||
else
|
||||
watch_clear_pixel(0, grid_loc - 5);
|
||||
break;
|
||||
case 11:
|
||||
if (obstacle)
|
||||
watch_set_pixel(1, 6);
|
||||
else
|
||||
watch_clear_pixel(1, 6);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void stop_jumping(endless_runner_state_t *state) {
|
||||
game_state.jump_state = NOT_JUMPING;
|
||||
display_ball(game_state.jump_state != NOT_JUMPING);
|
||||
if (state -> soundOn){
|
||||
if (game_state.success_jump)
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C5, 60);
|
||||
else
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C3, 60);
|
||||
}
|
||||
game_state.success_jump = false;
|
||||
}
|
||||
|
||||
static void display_obstacles(endless_runner_state_t *state) {
|
||||
for (int i = 0; i < NUM_GRID; i++) {
|
||||
// Use a bitmask to isolate each bit and shift it to the least significant position
|
||||
uint32_t mask = 1 << ((_num_bits_obst_pattern - 1) - i);
|
||||
bool obstacle = (game_state.obst_pattern & mask) >> ((_num_bits_obst_pattern - 1) - i);
|
||||
display_obstacle(obstacle, i, state);
|
||||
}
|
||||
game_state.obst_pattern = game_state.obst_pattern << 1;
|
||||
game_state.obst_indx++;
|
||||
if (game_state.fuel_mode) {
|
||||
if (game_state.obst_indx >= (_num_bits_obst_pattern / 2)) {
|
||||
game_state.obst_indx = 0;
|
||||
game_state.obst_pattern = get_random_fuel(game_state.obst_pattern);
|
||||
}
|
||||
}
|
||||
else if (game_state.obst_indx >= _num_bits_obst_pattern - NUM_GRID) {
|
||||
game_state.obst_indx = 0;
|
||||
game_state.obst_pattern = get_random_legal(game_state.obst_pattern, state -> difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
static void update_game(endless_runner_state_t *state, uint8_t subsecond) {
|
||||
uint8_t curr_jump_frame = 0;
|
||||
if (game_state.sec_before_moves != 0) {
|
||||
if (subsecond == 0) --game_state.sec_before_moves;
|
||||
return;
|
||||
}
|
||||
display_obstacles(state);
|
||||
switch (game_state.jump_state)
|
||||
{
|
||||
case NOT_JUMPING:
|
||||
if (game_state.fuel_mode) {
|
||||
for (int i = 0; i < JUMP_FRAMES_FUEL_RECHARGE; i++)
|
||||
{
|
||||
if(game_state.fuel >= JUMP_FRAMES_FUEL || (state -> difficulty == DIFF_FUEL_1 && !game_state.fuel))
|
||||
break;
|
||||
game_state.fuel++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case JUMPING_FINAL_FRAME:
|
||||
stop_jumping(state);
|
||||
break;
|
||||
default:
|
||||
if (game_state.fuel_mode) {
|
||||
if (!game_state.fuel)
|
||||
game_state.jump_state = JUMPING_FINAL_FRAME;
|
||||
else
|
||||
game_state.fuel--;
|
||||
if (!watch_get_pin_level(BTN_ALARM) && !watch_get_pin_level(BTN_LIGHT)) stop_jumping(state);
|
||||
}
|
||||
else {
|
||||
curr_jump_frame = game_state.jump_state - NOT_JUMPING;
|
||||
if (curr_jump_frame >= JUMP_FRAMES_EASY || (state -> difficulty >= DIFF_NORM && curr_jump_frame >= JUMP_FRAMES))
|
||||
game_state.jump_state = JUMPING_FINAL_FRAME;
|
||||
else
|
||||
game_state.jump_state++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (game_state.jump_state == NOT_JUMPING && (game_state.loc_2_on || game_state.loc_3_on)) {
|
||||
delay_ms(200); // To show the player jumping onto the obstacle before displaying the lose screen.
|
||||
display_lose_screen(state);
|
||||
}
|
||||
else if (game_state.fuel_mode)
|
||||
display_fuel(subsecond, state -> difficulty);
|
||||
}
|
||||
|
||||
void endless_runner_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(endless_runner_state_t));
|
||||
memset(*context_ptr, 0, sizeof(endless_runner_state_t));
|
||||
endless_runner_state_t *state = (endless_runner_state_t *)*context_ptr;
|
||||
state->difficulty = DIFF_NORM;
|
||||
}
|
||||
}
|
||||
|
||||
void endless_runner_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
bool endless_runner_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
endless_runner_state_t *state = (endless_runner_state_t *)context;
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
check_and_reset_hi_score(state);
|
||||
if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
display_title(state);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
switch (game_state.curr_screen)
|
||||
{
|
||||
case SCREEN_TITLE:
|
||||
case SCREEN_LOSE:
|
||||
break;
|
||||
default:
|
||||
update_game(state, event.subsecond);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
if (game_state.curr_screen == SCREEN_TITLE)
|
||||
begin_playing(state);
|
||||
else if (game_state.curr_screen == SCREEN_LOSE)
|
||||
display_title(state);
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
if (game_state.curr_screen == SCREEN_TITLE)
|
||||
change_difficulty(state);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
if (game_state.curr_screen == SCREEN_PLAYING && game_state.jump_state == NOT_JUMPING){
|
||||
if (game_state.fuel_mode && !game_state.fuel) break;
|
||||
game_state.jump_state = JUMPING_START;
|
||||
display_ball(game_state.jump_state != NOT_JUMPING);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (game_state.curr_screen != SCREEN_PLAYING)
|
||||
toggle_sound(state);
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
if (game_state.curr_screen != SCREEN_TITLE)
|
||||
display_title(state);
|
||||
break;
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
display_time(watch_rtc_get_date_time(), settings->bit.clock_mode_24h);
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void endless_runner_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
62
movement/watch_faces/complication/endless_runner_face.h
Normal file
62
movement/watch_faces/complication/endless_runner_face.h
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 <David Volovskiy>
|
||||
*
|
||||
* 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 ENDLESS_RUNNER_FACE_H_
|
||||
#define ENDLESS_RUNNER_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
ENDLESS_RUNNER face
|
||||
|
||||
This is a basic endless-runner, like the [Chrome Dino game](https://en.wikipedia.org/wiki/Dinosaur_Game).
|
||||
On the title screen, you can select a difficulty by long-pressing LIGHT or toggle sound by long-pressing ALARM.
|
||||
LED or ALARM are used to jump.
|
||||
High-score is displayed on the top-right on the title screen. During a game, the current score is displayed.
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
uint16_t hi_score : 10;
|
||||
uint8_t difficulty : 3;
|
||||
uint8_t month_last_hi_score : 4;
|
||||
uint8_t year_last_hi_score : 6;
|
||||
uint8_t soundOn : 1;
|
||||
/* 24 bits, likely aligned to 32 bits = 4 bytes */
|
||||
} endless_runner_state_t;
|
||||
|
||||
void endless_runner_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void endless_runner_face_activate(movement_settings_t *settings, void *context);
|
||||
bool endless_runner_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void endless_runner_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define endless_runner_face ((const watch_face_t){ \
|
||||
endless_runner_face_setup, \
|
||||
endless_runner_face_activate, \
|
||||
endless_runner_face_loop, \
|
||||
endless_runner_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // ENDLESS_RUNNER_FACE_H_
|
||||
|
396
movement/watch_faces/complication/higher_lower_game_face.c
Executable file
396
movement/watch_faces/complication/higher_lower_game_face.c
Executable file
|
@ -0,0 +1,396 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Chris Ellis
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
// Emulator only: need time() to seed the random number generator.
|
||||
#if __EMSCRIPTEN__
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "higher_lower_game_face.h"
|
||||
#include "watch_private_display.h"
|
||||
|
||||
#define TITLE_TEXT "Hi-Lo"
|
||||
#define GAME_BOARD_SIZE 6
|
||||
#define MAX_BOARDS 40
|
||||
#define GUESSES_PER_SCREEN 5
|
||||
#define WIN_SCORE (MAX_BOARDS * GUESSES_PER_SCREEN)
|
||||
#define STATUS_DISPLAY_START 0
|
||||
#define BOARD_SCORE_DISPLAY_START 2
|
||||
#define BOARD_DISPLAY_START 4
|
||||
#define BOARD_DISPLAY_END 9
|
||||
#define MIN_CARD_VALUE 2
|
||||
#define MAX_CARD_VALUE 14
|
||||
#define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1)
|
||||
#define CARD_SUIT_COUNT 4
|
||||
#define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT)
|
||||
#define FLIP_BOARD_DIRECTION false
|
||||
|
||||
typedef struct card_t {
|
||||
uint8_t value;
|
||||
bool revealed;
|
||||
} card_t;
|
||||
|
||||
typedef enum {
|
||||
A, B, C, D, E, F, G
|
||||
} segment_t;
|
||||
|
||||
typedef enum {
|
||||
HL_GUESS_EQUAL,
|
||||
HL_GUESS_HIGHER,
|
||||
HL_GUESS_LOWER
|
||||
} guess_t;
|
||||
|
||||
typedef enum {
|
||||
HL_GS_TITLE_SCREEN,
|
||||
HL_GS_GUESSING,
|
||||
HL_GS_WIN,
|
||||
HL_GS_LOSE,
|
||||
HL_GS_SHOW_SCORE,
|
||||
} game_state_t;
|
||||
|
||||
static game_state_t game_state = HL_GS_TITLE_SCREEN;
|
||||
static card_t game_board[GAME_BOARD_SIZE] = {0};
|
||||
static uint8_t guess_position = 0;
|
||||
static uint8_t score = 0;
|
||||
static uint8_t completed_board_count = 0;
|
||||
static uint8_t deck[DECK_SIZE] = {0};
|
||||
static uint8_t current_card = 0;
|
||||
|
||||
static uint8_t generate_random_number(uint8_t num_values) {
|
||||
// Emulator: use rand. Hardware: use arc4random.
|
||||
#if __EMSCRIPTEN__
|
||||
return rand() % num_values;
|
||||
#else
|
||||
return arc4random_uniform(num_values);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void stack_deck(void) {
|
||||
for (size_t i = 0; i < CARD_RANK_COUNT; i++) {
|
||||
for (size_t j = 0; j < CARD_SUIT_COUNT; j++)
|
||||
deck[(i * CARD_SUIT_COUNT) + j] = MIN_CARD_VALUE + i;
|
||||
}
|
||||
}
|
||||
|
||||
static void shuffle_deck(void) {
|
||||
// Randomize shuffle with Fisher Yates
|
||||
size_t i;
|
||||
size_t j;
|
||||
uint8_t tmp;
|
||||
|
||||
for (i = DECK_SIZE - 1; i > 0; i--) {
|
||||
j = generate_random_number(0xFF) % (i + 1);
|
||||
tmp = deck[j];
|
||||
deck[j] = deck[i];
|
||||
deck[i] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_deck(void) {
|
||||
current_card = 0;
|
||||
stack_deck();
|
||||
shuffle_deck();
|
||||
}
|
||||
|
||||
static uint8_t get_next_card(void) {
|
||||
if (current_card >= DECK_SIZE)
|
||||
reset_deck();
|
||||
return deck[current_card++];
|
||||
}
|
||||
|
||||
static void reset_board(bool first_round) {
|
||||
// First card is random on the first board, and carried over from the last position on subsequent boards
|
||||
const uint8_t first_card_value = first_round
|
||||
? get_next_card()
|
||||
: game_board[GAME_BOARD_SIZE - 1].value;
|
||||
|
||||
game_board[0].value = first_card_value;
|
||||
game_board[0].revealed = true; // Always reveal first card
|
||||
|
||||
// Fill remainder of board
|
||||
for (size_t i = 1; i < GAME_BOARD_SIZE; ++i) {
|
||||
game_board[i] = (card_t) {
|
||||
.value = get_next_card(),
|
||||
.revealed = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static void init_game(void) {
|
||||
watch_clear_display();
|
||||
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);
|
||||
watch_display_string("GA", STATUS_DISPLAY_START);
|
||||
reset_deck();
|
||||
reset_board(true);
|
||||
score = 0;
|
||||
completed_board_count = 0;
|
||||
guess_position = 1;
|
||||
}
|
||||
|
||||
static void set_segment_at_position(segment_t segment, uint8_t position) {
|
||||
const uint64_t position_segment_data = (Segment_Map[position] >> (8 * (uint8_t) segment)) & 0xFF;
|
||||
const uint8_t com_pin = position_segment_data >> 6;
|
||||
const uint8_t seg = position_segment_data & 0x3F;
|
||||
watch_set_pixel(com_pin, seg);
|
||||
}
|
||||
|
||||
static void render_board_position(size_t board_position) {
|
||||
const size_t display_position = FLIP_BOARD_DIRECTION
|
||||
? BOARD_DISPLAY_START + board_position
|
||||
: BOARD_DISPLAY_END - board_position;
|
||||
const bool revealed = game_board[board_position].revealed;
|
||||
|
||||
//// Current position indicator spot
|
||||
//if (board_position == guess_position) {
|
||||
// watch_display_character('-', display_position);
|
||||
// return;
|
||||
//}
|
||||
|
||||
if (!revealed) {
|
||||
// Higher or lower indicator (currently just an empty space)
|
||||
watch_display_character(' ', display_position);
|
||||
//set_segment_at_position(F, display_position);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t value = game_board[board_position].value;
|
||||
switch (value) {
|
||||
case 14: // A (≡)
|
||||
watch_display_character(' ', display_position);
|
||||
set_segment_at_position(A, display_position);
|
||||
set_segment_at_position(D, display_position);
|
||||
set_segment_at_position(G, display_position);
|
||||
break;
|
||||
case 13: // K (=)
|
||||
watch_display_character(' ', display_position);
|
||||
set_segment_at_position(A, display_position);
|
||||
set_segment_at_position(D, display_position);
|
||||
break;
|
||||
case 12: // Q (-)
|
||||
watch_display_character('-', display_position);
|
||||
break;
|
||||
default: {
|
||||
const char display_char = (value - MIN_CARD_VALUE) + '0';
|
||||
watch_display_character(display_char, display_position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void render_board(void) {
|
||||
for (size_t i = 0; i < GAME_BOARD_SIZE; ++i) {
|
||||
render_board_position(i);
|
||||
}
|
||||
}
|
||||
|
||||
static void render_board_count(void) {
|
||||
// Render completed boards (screens)
|
||||
char buf[3] = {0};
|
||||
snprintf(buf, sizeof(buf), "%2hhu", completed_board_count);
|
||||
watch_display_string(buf, BOARD_SCORE_DISPLAY_START);
|
||||
}
|
||||
|
||||
static void render_final_score(void) {
|
||||
watch_display_string("SC", STATUS_DISPLAY_START);
|
||||
char buf[7] = {0};
|
||||
const uint8_t complete_boards = score / GUESSES_PER_SCREEN;
|
||||
snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score);
|
||||
watch_set_colon();
|
||||
watch_display_string(buf, BOARD_DISPLAY_START);
|
||||
}
|
||||
|
||||
static guess_t get_answer(void) {
|
||||
if (guess_position < 1 || guess_position > GAME_BOARD_SIZE)
|
||||
return HL_GUESS_EQUAL; // Maybe add an error state, shouldn't ever hit this.
|
||||
|
||||
game_board[guess_position].revealed = true;
|
||||
const uint8_t previous_value = game_board[guess_position - 1].value;
|
||||
const uint8_t current_value = game_board[guess_position].value;
|
||||
|
||||
if (current_value > previous_value)
|
||||
return HL_GUESS_HIGHER;
|
||||
else if (current_value < previous_value)
|
||||
return HL_GUESS_LOWER;
|
||||
else
|
||||
return HL_GUESS_EQUAL;
|
||||
}
|
||||
|
||||
static void do_game_loop(guess_t user_guess) {
|
||||
switch (game_state) {
|
||||
case HL_GS_TITLE_SCREEN:
|
||||
init_game();
|
||||
render_board();
|
||||
render_board_count();
|
||||
game_state = HL_GS_GUESSING;
|
||||
break;
|
||||
case HL_GS_GUESSING: {
|
||||
const guess_t answer = get_answer();
|
||||
|
||||
// Render answer indicator
|
||||
switch (answer) {
|
||||
case HL_GUESS_EQUAL:
|
||||
watch_display_string("==", STATUS_DISPLAY_START);
|
||||
break;
|
||||
case HL_GUESS_HIGHER:
|
||||
watch_display_string("HI", STATUS_DISPLAY_START);
|
||||
break;
|
||||
case HL_GUESS_LOWER:
|
||||
watch_display_string("LO", STATUS_DISPLAY_START);
|
||||
break;
|
||||
}
|
||||
|
||||
// Scoring
|
||||
if (answer == user_guess) {
|
||||
score++;
|
||||
} else if (answer == HL_GUESS_EQUAL) {
|
||||
// No score for two consecutive identical cards
|
||||
} else {
|
||||
// Incorrect guess, game over
|
||||
watch_display_string("GO", STATUS_DISPLAY_START);
|
||||
game_board[guess_position].revealed = true;
|
||||
render_board_position(guess_position);
|
||||
game_state = HL_GS_LOSE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (score >= WIN_SCORE) {
|
||||
// Win, perhaps some kind of animation sequence?
|
||||
watch_display_string("WI", STATUS_DISPLAY_START);
|
||||
watch_display_string(" ", BOARD_SCORE_DISPLAY_START);
|
||||
watch_display_string("------", BOARD_DISPLAY_START);
|
||||
game_state = HL_GS_WIN;
|
||||
return;
|
||||
}
|
||||
|
||||
// Next guess position
|
||||
const bool final_board_guess = guess_position == GAME_BOARD_SIZE - 1;
|
||||
if (final_board_guess) {
|
||||
// Seed new board
|
||||
completed_board_count++;
|
||||
render_board_count();
|
||||
guess_position = 1;
|
||||
reset_board(false);
|
||||
render_board();
|
||||
} else {
|
||||
guess_position++;
|
||||
render_board_position(guess_position - 1);
|
||||
render_board_position(guess_position);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HL_GS_WIN:
|
||||
case HL_GS_LOSE:
|
||||
// Show score screen on button press from either state
|
||||
watch_clear_display();
|
||||
render_final_score();
|
||||
game_state = HL_GS_SHOW_SCORE;
|
||||
break;
|
||||
case HL_GS_SHOW_SCORE:
|
||||
watch_clear_display();
|
||||
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);
|
||||
watch_display_string("GA", STATUS_DISPLAY_START);
|
||||
game_state = HL_GS_TITLE_SCREEN;
|
||||
break;
|
||||
default:
|
||||
watch_display_string("ERROR", BOARD_DISPLAY_START);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void light_button_handler(void) {
|
||||
do_game_loop(HL_GUESS_HIGHER);
|
||||
}
|
||||
|
||||
static void alarm_button_handler(void) {
|
||||
do_game_loop(HL_GUESS_LOWER);
|
||||
}
|
||||
|
||||
void higher_lower_game_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(higher_lower_game_face_state_t));
|
||||
memset(*context_ptr, 0, sizeof(higher_lower_game_face_state_t));
|
||||
// Do any one-time tasks in here; the inside of this conditional happens only at boot.
|
||||
memset(game_board, 0, sizeof(game_board));
|
||||
}
|
||||
// Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.
|
||||
}
|
||||
|
||||
void higher_lower_game_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
higher_lower_game_face_state_t *state = (higher_lower_game_face_state_t *) context;
|
||||
(void) state;
|
||||
// Handle any tasks related to your watch face coming on screen.
|
||||
game_state = HL_GS_TITLE_SCREEN;
|
||||
}
|
||||
|
||||
bool higher_lower_game_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
higher_lower_game_face_state_t *state = (higher_lower_game_face_state_t *) context;
|
||||
(void) state;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
// Show your initial UI here.
|
||||
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);
|
||||
watch_display_string("GA", STATUS_DISPLAY_START);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
// If needed, update your display here.
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
light_button_handler();
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
// Don't trigger light
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
alarm_button_handler();
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
// Your watch face will receive this event after a period of inactivity. If it makes sense to resign,
|
||||
// you may uncomment this line to move back to the first watch face in the list:
|
||||
// movement_move_to_face(0);
|
||||
break;
|
||||
default:
|
||||
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 higher_lower_game_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
|
||||
// handle any cleanup before your watch face goes off-screen.
|
||||
}
|
106
movement/watch_faces/complication/higher_lower_game_face.h
Executable file
106
movement/watch_faces/complication/higher_lower_game_face.h
Executable file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Chris Ellis
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef HIGHER_LOWER_GAME_FACE_H_
|
||||
#define HIGHER_LOWER_GAME_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* Higher-Lower game face
|
||||
* ======================
|
||||
*
|
||||
* A game face based on the "higher-lower" card game where the objective is to correctly guess if the next card will
|
||||
* be higher or lower than the last revealed cards.
|
||||
*
|
||||
* Game Flow:
|
||||
* - When the face is selected, the "Hi-Lo" "Title" screen will be displayed, and the status indicator will display "GA" for game
|
||||
* - Pressing `ALARM` or `LIGHT` will start the game and proceed to the "Guessing" screen
|
||||
* - The first card will be revealed and the player must now make a guess
|
||||
* - A player can guess `Higher` by pressing the `LIGHT` button, and `Lower` by pressing the `ALARM` button
|
||||
* - The status indicator will show the result of the guess: HI (Higher), LO (Lower), or == (Equal)
|
||||
* - There are five guesses to make on each game screen, once the end of the screen is reached, a new screen
|
||||
* will be started, with the last revealed card carried over
|
||||
* - The number of completed screens is displayed in the top right (see Scoring)
|
||||
* - If the player has guessed correctly, the score is updated and play continues (see Scoring)
|
||||
* - If the player has guessed incorrectly, the status will change to GO (Game Over)
|
||||
* - The current card will be revealed
|
||||
* - Pressing `ALARM` or `LIGHT` will transition to the "Score" screen
|
||||
* - If the game is won, the status indicator will display "WI" and the "Win" screen will be displayed
|
||||
* - Pressing `ALARM` or `LIGHT` will transition to the "Score" screen
|
||||
* - The status indicator will change to "SC" when the final score is displayed
|
||||
* - The number of completed game screens will be displayed on using the first two digits
|
||||
* - The number of correct guesses will be displayed using the final three digits
|
||||
* - E.g. "13: 063" represents 13 completed screens, with 63 correct guesses
|
||||
* - Pressing `ALARM` or `LIGHT` while on the "Score" screen will transition to back to the "Title" screen
|
||||
*
|
||||
* Scoring:
|
||||
* - If the player guesses correctly (HI/LO) a point is gained
|
||||
* - If the player guesses incorrectly the game ends
|
||||
* - Unless the revealed card is equal (==) to the last card, in which case play continues, but no point is gained
|
||||
* - If the player completes 40 screens full of cards, the game ends and a win screen is displayed
|
||||
*
|
||||
* Misc:
|
||||
* The face tries to remain true to the spirit of using "cards"; to cope with the display limitations I've arrived at
|
||||
* the following mapping of card values to screen display, but am open to better suggestions:
|
||||
*
|
||||
* Thanks to voloved for adding deck shuffling and drawing!
|
||||
*
|
||||
* | Cards | |
|
||||
* |---------|--------------------------|
|
||||
* | Value |2|3|4|5|6|7|8|9|10|J|Q|K|A|
|
||||
* | Display |0|1|2|3|4|5|6|7|8 |9|-|=|≡|
|
||||
*
|
||||
* A previous alternative can be found in the git history:
|
||||
* | Cards | |
|
||||
* |---------|--------------------------|
|
||||
* | Value |2|3|4|5|6|7|8|9|10|J|Q|K|A|
|
||||
* | Display |2|3|4|5|6|7|8|9| 0|-|=|≡|H|
|
||||
*
|
||||
*
|
||||
* Future Ideas:
|
||||
* - Add sounds
|
||||
* - Save/Display high score
|
||||
* - Add a "Win" animation
|
||||
* - Consider using lap indicator for larger score limit
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
// Anything you need to keep track of, put it here!
|
||||
} higher_lower_game_face_state_t;
|
||||
|
||||
void higher_lower_game_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void higher_lower_game_face_activate(movement_settings_t *settings, void *context);
|
||||
bool higher_lower_game_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void higher_lower_game_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define higher_lower_game_face ((const watch_face_t){ \
|
||||
higher_lower_game_face_setup, \
|
||||
higher_lower_game_face_activate, \
|
||||
higher_lower_game_face_loop, \
|
||||
higher_lower_game_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // HIGHER_LOWER_GAME_FACE_H_
|
472
movement/watch_faces/complication/menstrual_cycle_face.c
Normal file
472
movement/watch_faces/complication/menstrual_cycle_face.c
Normal file
|
@ -0,0 +1,472 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Joseph Borne Komosa | @jokomo24
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*
|
||||
* Menstrual Cycle Face
|
||||
*
|
||||
* Background:
|
||||
*
|
||||
* I discovered the Casio F-91W through my partner, appreciated the retro aesthetic of the watch,
|
||||
* and got one for myself. Soon afterward I discovered the Sensor Watch project and ordered two boards!
|
||||
* I introduced the Sensor Watch to my partner who inquired whether she could track her menstrual cycle.
|
||||
* So I decided to implement a menstrual cycle watch face that also calculates the peak fertility window
|
||||
* using The Calendar Method. While this information may be useful when attempting to achieve or avoid
|
||||
* pregnancy, it is important to understand that these are rough estimates at best.
|
||||
*
|
||||
* How to use:
|
||||
*
|
||||
* 1. To begin tracking, go to 'Last Period' page and toggle the alarm button to the number of days since
|
||||
* the last, most recent, period and hold the alarm button to enter. This will perform the following actions:
|
||||
* - Store the corresponding date as the 'first' period in order to calculate the total_days_tracked.
|
||||
* - Turn on the Signal Indicator to signify that tracking has been activated.
|
||||
* - Deactivate this page and instead show the ticking animation.
|
||||
* - Adjust the days left in the 'Period in <num> Days' page accordingly.
|
||||
* - Activate the 'Period Is Here' page and no longer display 'NA'. To prevent accidental user entry,
|
||||
* the page will display the ticking animation until ten days have passed since the date of the last
|
||||
* period entered.
|
||||
* - Activate the 'Peak Fertility' page to begin showing the estimated window,
|
||||
* as well as display the Alarm Indicator, on this page and on the main 'Period in <num> Days' page,
|
||||
* whenever the current date falls within the Peak Fertility Window.
|
||||
*
|
||||
* 2. Toggle and enter 'y' in the 'Period Is Here' page on the day of every sequential period afterward.
|
||||
* DO NOT FORGET TO DO SO!
|
||||
* - If forgotten, the data will become inaccurate and tracking will need to be reset! -> (FIXME, allow one to enter a 'missed' period using the 'Last Period' page).
|
||||
* This will perform the following actions:
|
||||
* - Calculate this completed cycle's length and reevaluate the shortest and longest cycle variables.
|
||||
* - Increment total_cycles by one.
|
||||
* - Recalculate and save the average cycle for 'Average Cycle' page.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "menstrual_cycle_face.h"
|
||||
#include "watch.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
#define TYPICAL_AVG_CYC 28
|
||||
#define SECONDS_PER_DAY 86400
|
||||
|
||||
#define MENSTRUAL_CYCLE_FACE_NUM_PAGES (6)
|
||||
enum {
|
||||
period_in_num_days,
|
||||
average_cycle,
|
||||
peak_fertility_window,
|
||||
period_is_here,
|
||||
first_period,
|
||||
reset,
|
||||
} page_titles_e;
|
||||
const char menstrual_cycle_face_titles[MENSTRUAL_CYCLE_FACE_NUM_PAGES][11] = {
|
||||
"Prin day", // Period In <num> Days: Estimated days till the next period occurs
|
||||
"Av cycle ", // Average Cycle: The average number of days estimated per cycle
|
||||
"Peak Fert ", // Peak Fertility Window: The first and last day of month (displayed top & bottom right, respectively, once tracking) for the estimated window of fertility
|
||||
"Prishere ", // Period Is Here: Toggle and enter 'y' on the day the actual period occurs to improve Avg and Fert estimations
|
||||
"Last Per ", // Last Period: Enter the number of days since the last period to begin tracking from that corresponding date by storing it as the 'first'
|
||||
" Reset ", // Reset: Toggle and enter 'y' to reset tracking data
|
||||
};
|
||||
|
||||
/* Beep function */
|
||||
static inline void beep(movement_settings_t *settings) {
|
||||
if (settings->bit.button_should_sound)
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E8, 75);
|
||||
}
|
||||
|
||||
// Calculate the total number of days for which menstrual cycle tracking has been active
|
||||
static inline uint32_t total_days_tracked(menstrual_cycle_state_t *state) {
|
||||
|
||||
// If tracking has not yet been activated, return 0
|
||||
if (!(state->dates.reg))
|
||||
return 0;
|
||||
|
||||
// Otherwise, set the start date to the first day of the first tracked cycle
|
||||
watch_date_time date_time_start;
|
||||
date_time_start.unit.second = 0;
|
||||
date_time_start.unit.minute = 0;
|
||||
date_time_start.unit.hour = 0;
|
||||
date_time_start.unit.day = state->dates.bit.first_day;
|
||||
date_time_start.unit.month = state->dates.bit.first_month;
|
||||
date_time_start.unit.year = state->dates.bit.first_year;
|
||||
|
||||
// Get the current date and time
|
||||
watch_date_time date_time_now = watch_rtc_get_date_time();
|
||||
|
||||
// Convert the start date and current date to Unix time
|
||||
uint32_t unix_start = watch_utility_date_time_to_unix_time(date_time_start, state->utc_offset);
|
||||
uint32_t unix_now = watch_utility_date_time_to_unix_time(date_time_now, state->utc_offset);
|
||||
|
||||
// Calculate the total number of days and return it
|
||||
return (unix_now - unix_start) / SECONDS_PER_DAY;
|
||||
}
|
||||
|
||||
// Calculate the number of days until the next menstrual period
|
||||
static inline int8_t days_till_period(menstrual_cycle_state_t *state) {
|
||||
|
||||
// Calculate the number of days left until the next period based on the average cycle length and the number of cycles tracked
|
||||
int8_t days_left = (state->cycles.bit.average_cycle * (state->cycles.bit.total_cycles + 1)) - total_days_tracked(state);
|
||||
|
||||
// If the result is negative, return 0 (i.e., the period is expected to start today or has already started)
|
||||
return (days_left < 0) ? 0 : days_left;
|
||||
}
|
||||
|
||||
static inline void reset_tracking(menstrual_cycle_state_t *state) {
|
||||
|
||||
state->dates.bit.first_day = 0;
|
||||
state->dates.bit.first_month = 0;
|
||||
state->dates.bit.first_year = 0;
|
||||
|
||||
state->dates.bit.prev_day = 0;
|
||||
state->dates.bit.prev_month = 0;
|
||||
state->dates.bit.prev_year = 0;
|
||||
|
||||
state->cycles.bit.shortest_cycle = TYPICAL_AVG_CYC;
|
||||
state->cycles.bit.longest_cycle = TYPICAL_AVG_CYC;
|
||||
state->cycles.bit.average_cycle = TYPICAL_AVG_CYC;
|
||||
state->cycles.bit.total_cycles = 0;
|
||||
|
||||
state->dates.bit.reserved = 0;
|
||||
state->cycles.bit.reserved = 0;
|
||||
|
||||
watch_store_backup_data(state->dates.reg, state->backup_register_dt);
|
||||
watch_store_backup_data(state->cycles.reg, state->backup_register_cy);
|
||||
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
}
|
||||
|
||||
/*
|
||||
Fertility Window based on "The Calendar Method"
|
||||
Source: https://www.womenshealth.gov/pregnancy/you-get-pregnant/trying-conceive
|
||||
|
||||
The Calendar Method has several steps:
|
||||
|
||||
Step 1: Track the menstrual cycle for 8–12 months. One cycle is from the first day of one
|
||||
period until the first day of the next period. The average cycle is 28 days, but
|
||||
it may be as short as 24 days or as long as 38 days.
|
||||
Step 2: Subtract 18 from the number of days in the shortest menstrual cycle.
|
||||
Step 3: Subtract 11 from the number of days in the longest menstrual cycle.
|
||||
Step 4: Using a calendar, mark down the start of the next period (using previous instead). Count ahead by the number
|
||||
of days calculated in step 2. This is when peak fertility begins. Peak fertility ends
|
||||
at the number of days calculated in step 3.
|
||||
NOTE: Right now, the fertility window face displays its estimated window as soon as tracking is activated, although
|
||||
it is important to keep in mind that The Calendar Method states that peak accuracy of the window will be
|
||||
reached only after at least 8 months of tracking the menstrual cycle (can make it so that it only displays
|
||||
after total_days_tracked >= 8 months...but the info is interesting and should already be taken with the understanding that,
|
||||
in general, it is a rough estimation at best).
|
||||
*/
|
||||
typedef enum Fertile_Window {first_day, last_day} fertile_window;
|
||||
// Calculate the predicted starting or ending day of peak fertility
|
||||
static inline uint32_t get_day_pk_fert(menstrual_cycle_state_t *state, fertile_window which_day) {
|
||||
|
||||
// Get the date of the previous period
|
||||
watch_date_time date_prev_period;
|
||||
date_prev_period.unit.second = 0;
|
||||
date_prev_period.unit.minute = 0;
|
||||
date_prev_period.unit.hour = 0;
|
||||
date_prev_period.unit.day = state->dates.bit.prev_day;
|
||||
date_prev_period.unit.month = state->dates.bit.prev_month;
|
||||
date_prev_period.unit.year = state->dates.bit.prev_year;
|
||||
|
||||
// Convert the previous period date to Unix time
|
||||
uint32_t unix_prev_period = watch_utility_date_time_to_unix_time(date_prev_period, state->utc_offset);
|
||||
|
||||
// Calculate the Unix time of the predicted peak fertility day based on the length of the shortest/longest cycle
|
||||
uint32_t unix_pk_date;
|
||||
switch(which_day) {
|
||||
case first_day:
|
||||
unix_pk_date = unix_prev_period + ((state->cycles.bit.shortest_cycle - 18) * SECONDS_PER_DAY);
|
||||
break;
|
||||
case last_day:
|
||||
unix_pk_date = unix_prev_period + ((state->cycles.bit.longest_cycle - 11) * SECONDS_PER_DAY);
|
||||
break;
|
||||
}
|
||||
|
||||
// Convert the Unix time of the predicted peak fertility day to a date/time and return the day of the month
|
||||
return watch_utility_date_time_from_unix_time(unix_pk_date, state->utc_offset).unit.day;
|
||||
}
|
||||
|
||||
// Determine if today falls within the predicted peak fertility window
|
||||
static inline bool inside_fert_window(menstrual_cycle_state_t *state) {
|
||||
|
||||
// If tracking has not yet been activated, return false
|
||||
if (!(state->dates.reg))
|
||||
return false;
|
||||
|
||||
// Get the current date/time
|
||||
watch_date_time date_time_now = watch_rtc_get_date_time();
|
||||
|
||||
// Check if the current day falls between the first and last predicted peak fertility days
|
||||
if (get_day_pk_fert(state, first_day) > get_day_pk_fert(state, last_day)) { // We are crossing over the end of the month
|
||||
if (date_time_now.unit.day >= get_day_pk_fert(state, first_day) ||
|
||||
date_time_now.unit.day <= get_day_pk_fert(state, last_day))
|
||||
return true;
|
||||
}
|
||||
else if (date_time_now.unit.day >= get_day_pk_fert(state, first_day) &&
|
||||
date_time_now.unit.day <= get_day_pk_fert(state, last_day))
|
||||
return true;
|
||||
// If the current day does not fall within the predicted peak fertility window, return false
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the shortest and longest menstrual cycles based on the previous menstrual cycle
|
||||
static inline void update_shortest_longest_cycle(menstrual_cycle_state_t *state) {
|
||||
|
||||
// Get the date of the previous menstrual cycle
|
||||
watch_date_time date_prev_period;
|
||||
date_prev_period.unit.second = 0;
|
||||
date_prev_period.unit.minute = 0;
|
||||
date_prev_period.unit.hour = 0;
|
||||
date_prev_period.unit.day = state->dates.bit.prev_day;
|
||||
date_prev_period.unit.month = state->dates.bit.prev_month;
|
||||
date_prev_period.unit.year = state->dates.bit.prev_year;
|
||||
|
||||
// Convert the date of the previous menstrual cycle to UNIX time
|
||||
uint32_t unix_prev_period = watch_utility_date_time_to_unix_time(date_prev_period, state->utc_offset);
|
||||
|
||||
// Calculate the length of the current menstrual cycle
|
||||
uint8_t cycle_length = total_days_tracked(state) - (unix_prev_period / SECONDS_PER_DAY);
|
||||
|
||||
// Update the shortest or longest cycle length if necessary
|
||||
if (cycle_length < state->cycles.bit.shortest_cycle)
|
||||
state->cycles.bit.shortest_cycle = cycle_length;
|
||||
else if (cycle_length > state->cycles.bit.longest_cycle)
|
||||
state->cycles.bit.longest_cycle = cycle_length;
|
||||
}
|
||||
|
||||
void menstrual_cycle_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
|
||||
(void) watch_face_index;
|
||||
(void) settings;
|
||||
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(menstrual_cycle_state_t));
|
||||
memset(*context_ptr, 0, sizeof(menstrual_cycle_state_t));
|
||||
menstrual_cycle_state_t *state = ((menstrual_cycle_state_t *)*context_ptr);
|
||||
|
||||
state->dates.bit.first_day = 0;
|
||||
state->dates.bit.first_month = 0;
|
||||
state->dates.bit.first_year = 0;
|
||||
|
||||
state->dates.bit.prev_day = 0;
|
||||
state->dates.bit.prev_month = 0;
|
||||
state->dates.bit.prev_year = 0;
|
||||
|
||||
state->cycles.bit.shortest_cycle = TYPICAL_AVG_CYC;
|
||||
state->cycles.bit.longest_cycle = TYPICAL_AVG_CYC;
|
||||
state->cycles.bit.average_cycle = TYPICAL_AVG_CYC;
|
||||
state->cycles.bit.total_cycles = 0;
|
||||
|
||||
state->dates.bit.reserved = 0;
|
||||
state->cycles.bit.reserved = 0;
|
||||
|
||||
state->backup_register_dt = 0;
|
||||
state->backup_register_cy = 0;
|
||||
}
|
||||
|
||||
menstrual_cycle_state_t *state = ((menstrual_cycle_state_t *)*context_ptr);
|
||||
if (!(state->backup_register_dt && state->backup_register_cy)) {
|
||||
state->backup_register_dt = movement_claim_backup_register();
|
||||
state->backup_register_cy = movement_claim_backup_register();
|
||||
|
||||
if (state->backup_register_dt && state->backup_register_cy) {
|
||||
watch_store_backup_data(state->dates.reg, state->backup_register_dt);
|
||||
watch_store_backup_data(state->cycles.reg, state->backup_register_cy);
|
||||
}
|
||||
}
|
||||
else {
|
||||
state->dates.reg = watch_get_backup_data(state->backup_register_dt);
|
||||
state->cycles.reg = watch_get_backup_data(state->backup_register_cy);
|
||||
}
|
||||
}
|
||||
|
||||
void menstrual_cycle_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
menstrual_cycle_state_t *state = (menstrual_cycle_state_t *)context;
|
||||
state->period_today = 0;
|
||||
state->current_page = 0;
|
||||
state->reset_tracking = 0;
|
||||
state->utc_offset = movement_timezone_offsets[settings->bit.time_zone] * 60;
|
||||
movement_request_tick_frequency(4); // we need to manually blink some pixels
|
||||
}
|
||||
|
||||
bool menstrual_cycle_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
menstrual_cycle_state_t *state = (menstrual_cycle_state_t *)context;
|
||||
watch_date_time date_period;
|
||||
uint8_t current_page = state->current_page;
|
||||
uint8_t first_day_fert;
|
||||
uint8_t last_day_fert;
|
||||
uint32_t unix_now;
|
||||
uint32_t unix_prev_period;
|
||||
switch (event.event_type) {
|
||||
case EVENT_TICK:
|
||||
case EVENT_ACTIVATE:
|
||||
// Do nothing; handled below.
|
||||
break;
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
movement_move_to_next_face();
|
||||
return false;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
current_page = (current_page + 1) % MENSTRUAL_CYCLE_FACE_NUM_PAGES;
|
||||
state->current_page = current_page;
|
||||
state->days_prev_period = 0;
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
if (watch_tick_animation_is_running())
|
||||
watch_stop_tick_animation();
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
switch (current_page) {
|
||||
case period_in_num_days:
|
||||
break;
|
||||
case average_cycle:
|
||||
break;
|
||||
case peak_fertility_window:
|
||||
break;
|
||||
case period_is_here:
|
||||
if (state->period_today && total_days_tracked(state)) {
|
||||
// Calculate before updating date of last period
|
||||
update_shortest_longest_cycle(state);
|
||||
// Update the date of last period after calulating the, now previous, cycle length
|
||||
date_period = watch_rtc_get_date_time();
|
||||
state->dates.bit.prev_day = date_period.unit.day;
|
||||
state->dates.bit.prev_month = date_period.unit.month;
|
||||
state->dates.bit.prev_year = date_period.unit.year;
|
||||
// Calculate new cycle average
|
||||
state->cycles.bit.total_cycles += 1;
|
||||
state->cycles.bit.average_cycle = total_days_tracked(state) / state->cycles.bit.total_cycles;
|
||||
// Store the new data
|
||||
watch_store_backup_data(state->dates.reg, state->backup_register_dt);
|
||||
watch_store_backup_data(state->cycles.reg, state->backup_register_cy);
|
||||
state->period_today = !(state->period_today);
|
||||
beep(settings);
|
||||
}
|
||||
break;
|
||||
case first_period:
|
||||
// If tracking has not yet been activated
|
||||
if (!(state->dates.reg)) {
|
||||
unix_now = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), state->utc_offset);
|
||||
unix_prev_period = unix_now - (state->days_prev_period * SECONDS_PER_DAY);
|
||||
date_period = watch_utility_date_time_from_unix_time(unix_prev_period, state->utc_offset);
|
||||
state->dates.bit.first_day = date_period.unit.day;
|
||||
state->dates.bit.first_month = date_period.unit.month;
|
||||
state->dates.bit.first_year = date_period.unit.year;
|
||||
state->dates.bit.prev_day = date_period.unit.day;
|
||||
state->dates.bit.prev_month = date_period.unit.month;
|
||||
state->dates.bit.prev_year = date_period.unit.year;
|
||||
watch_store_backup_data(state->dates.reg, state->backup_register_dt);
|
||||
beep(settings);
|
||||
}
|
||||
break;
|
||||
case reset:
|
||||
if (state->reset_tracking) {
|
||||
reset_tracking(state);
|
||||
state->reset_tracking = !(state->reset_tracking);
|
||||
beep(settings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
switch (current_page) {
|
||||
case period_in_num_days:
|
||||
break;
|
||||
case average_cycle:
|
||||
break;
|
||||
case peak_fertility_window:
|
||||
break;
|
||||
case period_is_here:
|
||||
if (total_days_tracked(state))
|
||||
state->period_today = !(state->period_today);
|
||||
break;
|
||||
case first_period:
|
||||
if (!(state->dates.reg))
|
||||
state->days_prev_period = (state->days_prev_period > 99) ? 0 : state->days_prev_period + 1; // Cycle through pages to quickly reset to 0
|
||||
break;
|
||||
case reset:
|
||||
state->reset_tracking = !(state->reset_tracking);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
movement_move_to_face(0);
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
|
||||
watch_display_string((char *)menstrual_cycle_face_titles[current_page], 0);
|
||||
if (state->dates.reg)
|
||||
watch_set_indicator(WATCH_INDICATOR_SIGNAL); // signal that we are now in a tracking state
|
||||
|
||||
char buf[13];
|
||||
switch (current_page) {
|
||||
case period_in_num_days:
|
||||
sprintf(buf, "%2d", days_till_period(state));
|
||||
if (inside_fert_window(state))
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
watch_display_string(buf, 4);
|
||||
break;
|
||||
case average_cycle:
|
||||
sprintf(buf, "%2d", state->cycles.bit.average_cycle);
|
||||
watch_display_string(buf, 2);
|
||||
break;
|
||||
case peak_fertility_window:
|
||||
if (event.subsecond % 5 && state->dates.reg) { // blink active for 3 quarter-seconds
|
||||
first_day_fert = get_day_pk_fert(state, first_day);
|
||||
last_day_fert = get_day_pk_fert(state, last_day);
|
||||
sprintf(buf, "Fr%2d To %2d", first_day_fert, last_day_fert); // From: first day | To: last day
|
||||
if (inside_fert_window(state))
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
break;
|
||||
case period_is_here:
|
||||
if (event.subsecond % 5) { // blink active for 3 quarter-seconds
|
||||
if (!(state->dates.reg))
|
||||
watch_display_string("NA", 8); // Not Applicable: Do not allow period entry until tracking is activated...
|
||||
else if (state->period_today)
|
||||
watch_display_string("y", 9);
|
||||
else
|
||||
watch_display_string("n", 9);
|
||||
}
|
||||
break;
|
||||
case first_period:
|
||||
if (state->dates.reg) {
|
||||
if (!watch_tick_animation_is_running())
|
||||
watch_start_tick_animation(500); // Tracking activated
|
||||
}
|
||||
else if (event.subsecond % 5) { // blink active for 3 quarter-seconds
|
||||
sprintf(buf, "%2d", state->days_prev_period);
|
||||
watch_display_string(buf, 8);
|
||||
}
|
||||
break;
|
||||
case reset:
|
||||
// blink active for 3 quarter-seconds
|
||||
if (event.subsecond % 5 && state->reset_tracking)
|
||||
watch_display_string("y", 9);
|
||||
else if (event.subsecond % 5)
|
||||
watch_display_string("n", 9);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void menstrual_cycle_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
80
movement/watch_faces/complication/menstrual_cycle_face.h
Normal file
80
movement/watch_faces/complication/menstrual_cycle_face.h
Normal file
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Joseph Borne Komosa | @jokomo24
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef MENSTRUAL_CYCLE_FACE_H_
|
||||
#define MENSTRUAL_CYCLE_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
typedef struct {
|
||||
// Store the date of the 'first' and the total cycles since to calulate and store the average menstrual cycle.
|
||||
// Store the date of the previous, most recent, period to calculate the cycle length.
|
||||
// Store the shortest and longest cycle to calculate the fertility window for The Calender Method.
|
||||
// NOTE: Not thrilled about using two registers, but could not find a way to perform The Calender Method
|
||||
// without requiring both the 'first' and 'prev' dates.
|
||||
union {
|
||||
struct {
|
||||
uint8_t first_day : 5;
|
||||
uint8_t first_month : 4;
|
||||
uint8_t first_year : 6; // 0-63 (representing 2020-2083)
|
||||
uint8_t prev_day : 5;
|
||||
uint8_t prev_month : 4;
|
||||
uint8_t prev_year : 6; // 0-63 (representing 2020-2083)
|
||||
uint8_t reserved : 2; // left over bit space
|
||||
} bit;
|
||||
uint32_t reg; // Tracking's been activated if > 0
|
||||
} dates;
|
||||
union {
|
||||
struct {
|
||||
uint8_t shortest_cycle : 6; // For step 2 of The Calender Method
|
||||
uint8_t longest_cycle : 6; // For step 3 of The Calender Method
|
||||
uint8_t average_cycle : 6; // The average menstrual cycle lasts 28 days, but normal cycles can vary from 21 to 35 days
|
||||
uint16_t total_cycles : 11; // The total cycles (periods) entered since the start of tracking
|
||||
uint8_t reserved : 3; // left over bit space
|
||||
} bit;
|
||||
uint32_t reg;
|
||||
} cycles;
|
||||
uint8_t backup_register_dt;
|
||||
uint8_t backup_register_cy;
|
||||
uint8_t current_page;
|
||||
uint8_t days_prev_period;
|
||||
int32_t utc_offset;
|
||||
bool period_today;
|
||||
bool reset_tracking;
|
||||
} menstrual_cycle_state_t;
|
||||
|
||||
void menstrual_cycle_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void menstrual_cycle_face_activate(movement_settings_t *settings, void *context);
|
||||
bool menstrual_cycle_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void menstrual_cycle_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define menstrual_cycle_face ((const watch_face_t){ \
|
||||
menstrual_cycle_face_setup, \
|
||||
menstrual_cycle_face_activate, \
|
||||
menstrual_cycle_face_loop, \
|
||||
menstrual_cycle_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // MENSTRUAL_CYCLE_FACE_H_
|
263
movement/watch_faces/complication/metronome_face.c
Normal file
263
movement/watch_faces/complication/metronome_face.c
Normal file
|
@ -0,0 +1,263 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Austin Teets
|
||||
*
|
||||
* 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 "metronome_face.h"
|
||||
#include "watch.h"
|
||||
|
||||
static const int8_t _sound_seq_start[] = {BUZZER_NOTE_C8, 2, 0};
|
||||
static const int8_t _sound_seq_beat[] = {BUZZER_NOTE_C6, 2, 0};
|
||||
|
||||
void metronome_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(metronome_state_t));
|
||||
memset(*context_ptr, 0, sizeof(metronome_state_t));
|
||||
}
|
||||
}
|
||||
|
||||
void metronome_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
metronome_state_t *state = (metronome_state_t *)context;
|
||||
movement_request_tick_frequency(2);
|
||||
if (state->bpm == 0) {
|
||||
state->count = 4;
|
||||
state->bpm = 120;
|
||||
state->soundOn = true;
|
||||
}
|
||||
state->mode = metWait;
|
||||
state->correction = 0;
|
||||
state->setCur = hundred;
|
||||
}
|
||||
|
||||
static void _metronome_face_update_lcd(metronome_state_t *state) {
|
||||
char buf[11];
|
||||
if (state->soundOn) {
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
}
|
||||
sprintf(buf, "MN %d %03d%s", state->count, state->bpm, "bp");
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void _metronome_start_stop(metronome_state_t *state) {
|
||||
if (state->mode != metRun) {
|
||||
movement_request_tick_frequency(64);
|
||||
state->mode = metRun;
|
||||
watch_clear_display();
|
||||
double ticks = 3840.0 / (double)state->bpm;
|
||||
state->tick = (int) ticks;
|
||||
state->curTick = (int) ticks;
|
||||
state->halfBeat = (int)(state->tick/2);
|
||||
state->curCorrection = ticks - state->tick;
|
||||
state->correction = ticks - state->tick;
|
||||
state->curBeat = 1;
|
||||
} else {
|
||||
state->mode = metWait;
|
||||
movement_request_tick_frequency(2);
|
||||
_metronome_face_update_lcd(state);
|
||||
}
|
||||
}
|
||||
|
||||
static void _metronome_tick_beat(metronome_state_t *state) {
|
||||
char buf[11];
|
||||
if (state->soundOn) {
|
||||
if (state->curBeat == 1) {
|
||||
watch_buzzer_play_sequence((int8_t *)_sound_seq_start, NULL);
|
||||
} else {
|
||||
watch_buzzer_play_sequence((int8_t *)_sound_seq_beat, NULL);
|
||||
}
|
||||
}
|
||||
sprintf(buf, "MN %d %03d%s", state->count, state->bpm, "bp");
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void _metronome_event_tick(uint8_t subsecond, metronome_state_t *state) {
|
||||
(void) subsecond;
|
||||
|
||||
if (state->curCorrection >= 1) {
|
||||
state->curCorrection -= 1;
|
||||
state->curTick -= 1;
|
||||
}
|
||||
int diff = state->curTick - state->tick;
|
||||
if(diff == 0) {
|
||||
_metronome_tick_beat(state);
|
||||
state->curTick = 0;
|
||||
state->curCorrection += state->correction;
|
||||
if (state->curBeat < state->count ) {
|
||||
state->curBeat += 1;
|
||||
} else {
|
||||
state->curBeat = 1;
|
||||
}
|
||||
} else {
|
||||
if (state->curTick == state->halfBeat) {
|
||||
watch_clear_display();
|
||||
}
|
||||
state->curTick += 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void _metronome_setting_tick(uint8_t subsecond, metronome_state_t *state) {
|
||||
char buf[13];
|
||||
sprintf(buf, "MN %d %03d%s", state->count, state->bpm, "bp");
|
||||
if (subsecond%2 == 0) {
|
||||
switch (state->setCur) {
|
||||
case hundred:
|
||||
buf[5] = ' ';
|
||||
break;
|
||||
case ten:
|
||||
buf[6] = ' ';
|
||||
break;
|
||||
case one:
|
||||
buf[7] = ' ';
|
||||
break;
|
||||
case count:
|
||||
buf[3] = ' ';
|
||||
break;
|
||||
case alarm:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (state->setCur == alarm) {
|
||||
sprintf(buf, "MN 8eep%s", state->soundOn ? "On" : " -");
|
||||
}
|
||||
if (state->soundOn) {
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
}
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void _metronome_update_setting(metronome_state_t *state) {
|
||||
char buf[13];
|
||||
switch (state->setCur) {
|
||||
case hundred:
|
||||
if (state->bpm < 100) {
|
||||
state->bpm += 100;
|
||||
} else {
|
||||
state->bpm -= 100;
|
||||
}
|
||||
break;
|
||||
case ten:
|
||||
if ((state->bpm / 10) % 10 < 9) {
|
||||
state->bpm += 10;
|
||||
} else {
|
||||
state->bpm -= 90;
|
||||
}
|
||||
break;
|
||||
case one:
|
||||
if (state->bpm%10 < 9) {
|
||||
state->bpm += 1;
|
||||
} else {
|
||||
state->bpm -= 9;
|
||||
}
|
||||
break;
|
||||
case count:
|
||||
if (state->count < 9) {
|
||||
state->count += 1;
|
||||
} else {
|
||||
state->count = 2;
|
||||
}
|
||||
break;
|
||||
case alarm:
|
||||
state->soundOn = !state->soundOn;
|
||||
break;
|
||||
}
|
||||
sprintf(buf, "MN %d %03d%s", state->count % 10, state->bpm, "bp");
|
||||
if (state->setCur == alarm) {
|
||||
sprintf(buf, "MN 8eep%s", state->soundOn ? "On" : " -");
|
||||
}
|
||||
if (state->soundOn) {
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
}
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
bool metronome_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
metronome_state_t *state = (metronome_state_t *)context;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
_metronome_face_update_lcd(state);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (state->mode == metRun){
|
||||
_metronome_event_tick(event.subsecond, state);
|
||||
} else if (state->mode == setMenu) {
|
||||
_metronome_setting_tick(event.subsecond, state);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
if (state->mode == setMenu) {
|
||||
_metronome_update_setting(state);
|
||||
} else {
|
||||
_metronome_start_stop(state);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
if (state->mode == setMenu) {
|
||||
if (state->setCur < alarm) {
|
||||
state->setCur += 1;
|
||||
} else {
|
||||
state->setCur = hundred;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (state->mode != metRun && state->mode != setMenu) {
|
||||
movement_request_tick_frequency(2);
|
||||
state->mode = setMenu;
|
||||
_metronome_face_update_lcd(state);
|
||||
} else if (state->mode == setMenu) {
|
||||
state->mode = metWait;
|
||||
_metronome_face_update_lcd(state);
|
||||
}
|
||||
break;
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
movement_move_to_next_face();
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
if (state->mode != metRun) {
|
||||
movement_move_to_face(0);
|
||||
}
|
||||
break;
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void metronome_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
86
movement/watch_faces/complication/metronome_face.h
Normal file
86
movement/watch_faces/complication/metronome_face.h
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Austin Teets
|
||||
*
|
||||
* 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 METRONOME_FACE_H_
|
||||
#define METRONOME_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* A Metronome watch complication
|
||||
* Allows the user to set the BPM, counts per measure, beep sound on/off
|
||||
* Screen flashes on on the beat and off on the half beat (1/8th note)
|
||||
* Beep will sound high for downbeat and low for subsequent beats in measure
|
||||
* USE:
|
||||
* Press Alarm to start/stop metronome_face
|
||||
* Hold Alarm to enter settings menu
|
||||
* Short Light press will move through options
|
||||
* Short Alarm press will increment/toggle options
|
||||
* Long alarm press will exit options
|
||||
*/
|
||||
|
||||
typedef enum {
|
||||
metWait,
|
||||
metRun,
|
||||
setMenu
|
||||
} metronome_mode_t;
|
||||
|
||||
typedef enum {
|
||||
hundred,
|
||||
ten,
|
||||
one,
|
||||
count,
|
||||
alarm
|
||||
} setting_cursor_t;
|
||||
|
||||
typedef struct {
|
||||
// Anything you need to keep track of, put it here!
|
||||
uint8_t bpm;
|
||||
double correction;
|
||||
double curCorrection;
|
||||
int count;
|
||||
int tick;
|
||||
int curTick;
|
||||
int curBeat;
|
||||
int halfBeat;
|
||||
metronome_mode_t mode : 3;
|
||||
setting_cursor_t setCur : 4;
|
||||
bool soundOn;
|
||||
} metronome_state_t;
|
||||
|
||||
void metronome_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void metronome_face_activate(movement_settings_t *settings, void *context);
|
||||
bool metronome_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void metronome_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define metronome_face ((const watch_face_t){ \
|
||||
metronome_face_setup, \
|
||||
metronome_face_activate, \
|
||||
metronome_face_loop, \
|
||||
metronome_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // METRONOME_FACE_H_
|
||||
|
503
movement/watch_faces/complication/periodic_face.c
Normal file
503
movement/watch_faces/complication/periodic_face.c
Normal file
|
@ -0,0 +1,503 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 PrimmR
|
||||
* Copyright (c) 2024 David Volovskiy
|
||||
*
|
||||
* 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 "periodic_face.h"
|
||||
|
||||
#define FREQ_FAST 8
|
||||
#define FREQ 2
|
||||
|
||||
static bool _quick_ticks_running;
|
||||
static uint8_t _ts_ticks = 0;
|
||||
static int16_t _text_pos;
|
||||
static const char* _text_looping;
|
||||
static const char title_text[] = "Periodic Table";
|
||||
|
||||
void periodic_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(periodic_state_t));
|
||||
memset(*context_ptr, 0, sizeof(periodic_state_t));
|
||||
}
|
||||
}
|
||||
|
||||
void periodic_face_activate(movement_settings_t *settings, void *context)
|
||||
{
|
||||
(void)settings;
|
||||
periodic_state_t *state = (periodic_state_t *)context;
|
||||
|
||||
state->atomic_num = 0;
|
||||
state->mode = 0;
|
||||
state->selection_index = 0;
|
||||
_quick_ticks_running = false;
|
||||
movement_request_tick_frequency(FREQ);
|
||||
}
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char symbol[3];
|
||||
char name[14]; // Longest is Rutherfordium
|
||||
int16_t year_discovered; // Negative is BC
|
||||
uint16_t atomic_mass; // In units of 0.01 AMU
|
||||
uint16_t electronegativity; // In units of 0.01
|
||||
char group[3];
|
||||
} element;
|
||||
|
||||
typedef enum {
|
||||
SCREEN_TITLE = 0,
|
||||
SCREEN_ELEMENT,
|
||||
SCREEN_ATOMIC_MASS,
|
||||
SCREEN_DISCOVER_YEAR,
|
||||
SCREEN_ELECTRONEGATIVITY,
|
||||
SCREEN_FULL_NAME,
|
||||
SCREENS_COUNT
|
||||
} PeriodicScreens;
|
||||
|
||||
const char screen_name[SCREENS_COUNT][3] = {
|
||||
[SCREEN_ATOMIC_MASS] = "am",
|
||||
[SCREEN_DISCOVER_YEAR] = " y",
|
||||
[SCREEN_ELECTRONEGATIVITY] = "EL",
|
||||
[SCREEN_FULL_NAME] = " n",
|
||||
};
|
||||
|
||||
// Comments on the table denote symbols that cannot be displayed
|
||||
#define MAX_ELEMENT 118
|
||||
const element table[MAX_ELEMENT] = {
|
||||
{ .symbol = "H", .name = "Hydrogen", .year_discovered = 1671, .atomic_mass = 101, .electronegativity = 220, .group = " " },
|
||||
{ .symbol = "HE", .name = "Helium", .year_discovered = 1868, .atomic_mass = 400, .electronegativity = 0, .group = "0" },
|
||||
{ .symbol = "LI", .name = "Lithium", .year_discovered = 1817, .atomic_mass = 694, .electronegativity = 98, .group = "1" },
|
||||
{ .symbol = "BE", .name = "Beryllium", .year_discovered = 1798, .atomic_mass = 901, .electronegativity = 157, .group = "2" },
|
||||
{ .symbol = "B", .name = "Boron", .year_discovered = 1787, .atomic_mass = 1081, .electronegativity = 204, .group = "3" },
|
||||
{ .symbol = "C", .name = "Carbon", .year_discovered = -26000, .atomic_mass = 1201, .electronegativity = 255, .group = "4" },
|
||||
{ .symbol = "N", .name = "Nitrogen", .year_discovered = 1772, .atomic_mass = 1401, .electronegativity = 304, .group = "5" },
|
||||
{ .symbol = "O", .name = "Oxygen", .year_discovered = 1771, .atomic_mass = 1600, .electronegativity = 344, .group = "6" },
|
||||
{ .symbol = "F", .name = "Fluorine", .year_discovered = 1771, .atomic_mass = 1900, .electronegativity = 398, .group = "7" },
|
||||
{ .symbol = "NE", .name = "Neon", .year_discovered = 1898, .atomic_mass = 2018, .electronegativity = 0, .group = "0" },
|
||||
{ .symbol = "NA", .name = "Sodium", .year_discovered = 1702, .atomic_mass = 2299, .electronegativity = 93, .group = "1" },
|
||||
{ .symbol = "MG", .name = "Magnesium", .year_discovered = 1755, .atomic_mass = 2431, .electronegativity = 131, .group = "2" },
|
||||
{ .symbol = "AL", .name = "Aluminium", .year_discovered = 1746, .atomic_mass = 2698, .electronegativity = 161, .group = "3" },
|
||||
{ .symbol = "SI", .name = "Silicon", .year_discovered = 1739, .atomic_mass = 2809, .electronegativity = 190, .group = "4" },
|
||||
{ .symbol = "P", .name = "Phosphorus", .year_discovered = 1669, .atomic_mass = 3097, .electronegativity = 219, .group = "5" },
|
||||
{ .symbol = "S", .name = "Sulfur", .year_discovered = -2000, .atomic_mass = 3206, .electronegativity = 258, .group = "6" },
|
||||
{ .symbol = "CL", .name = "Chlorine", .year_discovered = 1774, .atomic_mass = 3545., .electronegativity = 316, .group = "7" },
|
||||
{ .symbol = "AR", .name = "Argon", .year_discovered = 1894, .atomic_mass = 3995., .electronegativity = 0, .group = "0" },
|
||||
{ .symbol = "K", .name = "Potassium", .year_discovered = 1702, .atomic_mass = 3910, .electronegativity = 82, .group = "1" },
|
||||
{ .symbol = "CA", .name = "Calcium", .year_discovered = 1739, .atomic_mass = 4008, .electronegativity = 100, .group = "2" },
|
||||
{ .symbol = "SC", .name = "Scandium", .year_discovered = 1879, .atomic_mass = 4496, .electronegativity = 136, .group = " T" },
|
||||
{ .symbol = "TI", .name = "Titanium", .year_discovered = 1791, .atomic_mass = 4787, .electronegativity = 154, .group = " T" },
|
||||
{ .symbol = "W", .name = "Vanadium", .year_discovered = 1801, .atomic_mass = 5094, .electronegativity = 163, .group = " T" },
|
||||
{ .symbol = "CR", .name = "Chromium", .year_discovered = 1797, .atomic_mass = 5200, .electronegativity = 166, .group = " T" },
|
||||
{ .symbol = "MN", .name = "Manganese", .year_discovered = 1774, .atomic_mass = 5494, .electronegativity = 155, .group = " T" },
|
||||
{ .symbol = "FE", .name = "Iron", .year_discovered = -5000, .atomic_mass = 5585, .electronegativity = 183, .group = " T" },
|
||||
{ .symbol = "CO", .name = "Cobalt", .year_discovered = 1735, .atomic_mass = 5893, .electronegativity = 188, .group = " T" },
|
||||
{ .symbol = "NI", .name = "Nickel", .year_discovered = 1751, .atomic_mass = 5869, .electronegativity = 191, .group = " T" },
|
||||
{ .symbol = "CU", .name = "Copper", .year_discovered = -9000, .atomic_mass = 6355, .electronegativity = 190, .group = " T" },
|
||||
{ .symbol = "ZN", .name = "Zinc", .year_discovered = -1000, .atomic_mass = 6538, .electronegativity = 165, .group = " T" },
|
||||
{ .symbol = "GA", .name = "Gallium", .year_discovered = 1875, .atomic_mass = 6972, .electronegativity = 181, .group = "3" },
|
||||
{ .symbol = "GE", .name = "Germanium", .year_discovered = 1886, .atomic_mass = 7263, .electronegativity = 201, .group = "4" },
|
||||
{ .symbol = "AS", .name = "Arsenic", .year_discovered = 300, .atomic_mass = 7492, .electronegativity = 218, .group = "5" },
|
||||
{ .symbol = "SE", .name = "Selenium", .year_discovered = 1817, .atomic_mass = 7897, .electronegativity = 255, .group = "6" },
|
||||
{ .symbol = "BR", .name = "Bromine", .year_discovered = 1825, .atomic_mass = 7990., .electronegativity = 296, .group = "7" },
|
||||
{ .symbol = "KR", .name = "Krypton", .year_discovered = 1898, .atomic_mass = 8380, .electronegativity = 300, .group = "0" },
|
||||
{ .symbol = "RB", .name = "Rubidium", .year_discovered = 1861, .atomic_mass = 8547, .electronegativity = 82, .group = "1" },
|
||||
{ .symbol = "SR", .name = "Strontium", .year_discovered = 1787, .atomic_mass = 8762, .electronegativity = 95, .group = "2" },
|
||||
{ .symbol = "Y", .name = "Yttrium", .year_discovered = 1794, .atomic_mass = 8891, .electronegativity = 122, .group = " T" },
|
||||
{ .symbol = "ZR", .name = "Zirconium", .year_discovered = 1789, .atomic_mass = 9122, .electronegativity = 133, .group = " T" },
|
||||
{ .symbol = "NB", .name = "Niobium", .year_discovered = 1801, .atomic_mass = 9291, .electronegativity = 160, .group = " T" },
|
||||
{ .symbol = "MO", .name = "Molybdenum", .year_discovered = 1778, .atomic_mass = 9595, .electronegativity = 216, .group = " T" },
|
||||
{ .symbol = "TC", .name = "Technetium", .year_discovered = 1937, .atomic_mass = 9700, .electronegativity = 190, .group = " T" },
|
||||
{ .symbol = "RU", .name = "Ruthenium", .year_discovered = 1844, .atomic_mass = 10107, .electronegativity = 220, .group = " T" },
|
||||
{ .symbol = "RH", .name = "Rhodium", .year_discovered = 1804, .atomic_mass = 10291, .electronegativity = 228, .group = " T" },
|
||||
{ .symbol = "PD", .name = "Palladium", .year_discovered = 1802, .atomic_mass = 10642, .electronegativity = 220, .group = " T" },
|
||||
{ .symbol = "AG", .name = "Silver", .year_discovered = -5000, .atomic_mass = 10787, .electronegativity = 193, .group = " T" },
|
||||
{ .symbol = "CD", .name = "Cadmium", .year_discovered = 1817, .atomic_mass = 11241, .electronegativity = 169, .group = " T" },
|
||||
{ .symbol = "IN", .name = "Indium", .year_discovered = 1863, .atomic_mass = 11482, .electronegativity = 178, .group = "3" },
|
||||
{ .symbol = "SN", .name = "Tin", .year_discovered = -3500, .atomic_mass = 11871, .electronegativity = 196, .group = "4" },
|
||||
{ .symbol = "SB", .name = "Antimony", .year_discovered = -3000, .atomic_mass = 12176, .electronegativity = 205, .group = "5" },
|
||||
{ .symbol = "TE", .name = "Tellurium", .year_discovered = 1782, .atomic_mass = 12760, .electronegativity = 210, .group = "6" },
|
||||
{ .symbol = "I", .name = "Iodine", .year_discovered = 1811, .atomic_mass = 12690, .electronegativity = 266, .group = "7" },
|
||||
{ .symbol = "XE", .name = "Xenon", .year_discovered = 1898, .atomic_mass = 13129, .electronegativity = 260, .group = "0" },
|
||||
{ .symbol = "CS", .name = "Caesium", .year_discovered = 1860, .atomic_mass = 13291, .electronegativity = 79, .group = "1" },
|
||||
{ .symbol = "BA", .name = "Barium", .year_discovered = 1772, .atomic_mass = 13733., .electronegativity = 89, .group = "2" },
|
||||
{ .symbol = "LA", .name = "Lanthanum", .year_discovered = 1838, .atomic_mass = 13891, .electronegativity = 110, .group = "1a" },
|
||||
{ .symbol = "CE", .name = "Cerium", .year_discovered = 1803, .atomic_mass = 14012, .electronegativity = 112, .group = "1a" },
|
||||
{ .symbol = "PR", .name = "Praseodymium", .year_discovered = 1885, .atomic_mass = 14091, .electronegativity = 113, .group = "1a" },
|
||||
{ .symbol = "ND", .name = "Neodymium", .year_discovered = 1841, .atomic_mass = 14424, .electronegativity = 114, .group = "1a" },
|
||||
{ .symbol = "PM", .name = "Promethium", .year_discovered = 1945, .atomic_mass = 14500, .electronegativity = 113, .group = "1a" },
|
||||
{ .symbol = "SM", .name = "Samarium", .year_discovered = 1879, .atomic_mass = 15036., .electronegativity = 117, .group = "1a" },
|
||||
{ .symbol = "EU", .name = "Europium", .year_discovered = 1896, .atomic_mass = 15196, .electronegativity = 120, .group = "1a" },
|
||||
{ .symbol = "GD", .name = "Gadolinium", .year_discovered = 1880, .atomic_mass = 15725, .electronegativity = 120, .group = "1a" },
|
||||
{ .symbol = "TB", .name = "Terbium", .year_discovered = 1843, .atomic_mass = 15893, .electronegativity = 120, .group = "1a" },
|
||||
{ .symbol = "DY", .name = "Dysprosium", .year_discovered = 1886, .atomic_mass = 16250, .electronegativity = 122, .group = "1a" },
|
||||
{ .symbol = "HO", .name = "Holmium", .year_discovered = 1878, .atomic_mass = 16493, .electronegativity = 123, .group = "1a" },
|
||||
{ .symbol = "ER", .name = "Erbium", .year_discovered = 1843, .atomic_mass = 16726, .electronegativity = 124, .group = "1a" },
|
||||
{ .symbol = "TM", .name = "Thulium", .year_discovered = 1879, .atomic_mass = 16893, .electronegativity = 125, .group = "1a" },
|
||||
{ .symbol = "YB", .name = "Ytterbium", .year_discovered = 1878, .atomic_mass = 17305, .electronegativity = 110, .group = "1a" },
|
||||
{ .symbol = "LU", .name = "Lutetium", .year_discovered = 1906, .atomic_mass = 17497, .electronegativity = 127, .group = "1a" },
|
||||
{ .symbol = "HF", .name = "Hafnium", .year_discovered = 1922, .atomic_mass = 17849, .electronegativity = 130, .group = " T" },
|
||||
{ .symbol = "TA", .name = "Tantalum", .year_discovered = 1802, .atomic_mass = 18095, .electronegativity = 150, .group = " T" },
|
||||
{ .symbol = "W", .name = "Tungsten", .year_discovered = 1781, .atomic_mass = 18384, .electronegativity = 236, .group = " T" },
|
||||
{ .symbol = "RE", .name = "Rhenium", .year_discovered = 1908, .atomic_mass = 18621, .electronegativity = 190, .group = " T" },
|
||||
{ .symbol = "OS", .name = "Osmium", .year_discovered = 1803, .atomic_mass = 19023, .electronegativity = 220, .group = " T" },
|
||||
{ .symbol = "IR", .name = "Iridium", .year_discovered = 1803, .atomic_mass = 19222, .electronegativity = 220, .group = " T" },
|
||||
{ .symbol = "PT", .name = "Platinum", .year_discovered = -600, .atomic_mass = 19508, .electronegativity = 228, .group = " T" },
|
||||
{ .symbol = "AU", .name = "Gold", .year_discovered = -6000, .atomic_mass = 19697, .electronegativity = 254, .group = " T" },
|
||||
{ .symbol = "HG", .name = "Mercury", .year_discovered = -1500, .atomic_mass = 20059, .electronegativity = 200, .group = " T" },
|
||||
{ .symbol = "TL", .name = "Thallium", .year_discovered = 1861, .atomic_mass = 20438, .electronegativity = 162, .group = "3" },
|
||||
{ .symbol = "PB", .name = "Lead", .year_discovered = -7000, .atomic_mass = 20720, .electronegativity = 187, .group = "4" },
|
||||
{ .symbol = "BI", .name = "Bismuth", .year_discovered = 1500, .atomic_mass = 20898, .electronegativity = 202, .group = "5" },
|
||||
{ .symbol = "PO", .name = "Polonium", .year_discovered = 1898, .atomic_mass = 20900, .electronegativity = 200, .group = "6" },
|
||||
{ .symbol = "AT", .name = "Astatine", .year_discovered = 1940, .atomic_mass = 21000, .electronegativity = 220, .group = "7" },
|
||||
{ .symbol = "RN", .name = "Radon", .year_discovered = 1899, .atomic_mass = 22200, .electronegativity = 220, .group = "0" },
|
||||
{ .symbol = "FR", .name = "Francium", .year_discovered = 1939, .atomic_mass = 22300, .electronegativity = 79, .group = "1" },
|
||||
{ .symbol = "RA", .name = "Radium", .year_discovered = 1898, .atomic_mass = 22600, .electronegativity = 90, .group = "2" },
|
||||
{ .symbol = "AC", .name = "Actinium", .year_discovered = 1902, .atomic_mass = 22700, .electronegativity = 110, .group = "Ac" },
|
||||
{ .symbol = "TH", .name = "Thorium", .year_discovered = 1829, .atomic_mass = 23204, .electronegativity = 130, .group = "Ac" },
|
||||
{ .symbol = "PA", .name = "Protactinium", .year_discovered = 1913, .atomic_mass = 23104, .electronegativity = 150, .group = "Ac" },
|
||||
{ .symbol = "U", .name = "Uranium", .year_discovered = 1789, .atomic_mass = 23803, .electronegativity = 138, .group = "Ac" },
|
||||
{ .symbol = "NP", .name = "Neptunium", .year_discovered = 1940, .atomic_mass = 23700, .electronegativity = 136, .group = "Ac" },
|
||||
{ .symbol = "PU", .name = "Plutonium", .year_discovered = 1941, .atomic_mass = 24400, .electronegativity = 128, .group = "Ac" },
|
||||
{ .symbol = "AM", .name = "Americium", .year_discovered = 1944, .atomic_mass = 24300, .electronegativity = 113, .group = "Ac" },
|
||||
{ .symbol = "CM", .name = "Curium", .year_discovered = 1944, .atomic_mass = 24700, .electronegativity = 128, .group = "Ac" },
|
||||
{ .symbol = "BK", .name = "Berkelium", .year_discovered = 1949, .atomic_mass = 24700, .electronegativity = 130, .group = "Ac" },
|
||||
{ .symbol = "CF", .name = "Californium", .year_discovered = 1950, .atomic_mass = 25100, .electronegativity = 130, .group = "Ac" },
|
||||
{ .symbol = "ES", .name = "Einsteinium", .year_discovered = 1952, .atomic_mass = 25200, .electronegativity = 130, .group = "Ac" },
|
||||
{ .symbol = "FM", .name = "Fermium", .year_discovered = 1953, .atomic_mass = 25700, .electronegativity = 130, .group = "Ac" },
|
||||
{ .symbol = "MD", .name = "Mendelevium", .year_discovered = 1955, .atomic_mass = 25800, .electronegativity = 130, .group = "Ac" },
|
||||
{ .symbol = "NO", .name = "Nobelium", .year_discovered = 1965, .atomic_mass = 25900, .electronegativity = 130, .group = "Ac" },
|
||||
{ .symbol = "LR", .name = "Lawrencium", .year_discovered = 1961, .atomic_mass = 26600, .electronegativity = 130, .group = "Ac" },
|
||||
{ .symbol = "RF", .name = "Rutherfordium", .year_discovered = 1969, .atomic_mass = 26700, .electronegativity = 0, .group = " T" },
|
||||
{ .symbol = "DB", .name = "Dubnium", .year_discovered = 1970, .atomic_mass = 26800, .electronegativity = 0, .group = " T" },
|
||||
{ .symbol = "SG", .name = "Seaborgium", .year_discovered = 1974, .atomic_mass = 26700, .electronegativity = 0, .group = " T" },
|
||||
{ .symbol = "BH", .name = "Bohrium", .year_discovered = 1981, .atomic_mass = 27000, .electronegativity = 0, .group = " T" },
|
||||
{ .symbol = "HS", .name = "Hassium", .year_discovered = 1984, .atomic_mass = 27100, .electronegativity = 0, .group = " T" },
|
||||
{ .symbol = "MT", .name = "Meitnerium", .year_discovered = 1982, .atomic_mass = 27800, .electronegativity = 0, .group = " T" },
|
||||
{ .symbol = "DS", .name = "Darmstadtium", .year_discovered = 1994, .atomic_mass = 28100, .electronegativity = 0, .group = " T" },
|
||||
{ .symbol = "RG", .name = "Roentgenium", .year_discovered = 1994, .atomic_mass = 28200, .electronegativity = 0, .group = " T" },
|
||||
{ .symbol = "CN", .name = "Copernicium", .year_discovered = 1996, .atomic_mass = 28500, .electronegativity = 0, .group = " T" },
|
||||
{ .symbol = "NH", .name = "Nihonium", .year_discovered = 2004, .atomic_mass = 28600, .electronegativity = 0, .group = "3" },
|
||||
{ .symbol = "FL", .name = "Flerovium", .year_discovered = 1999, .atomic_mass = 28900, .electronegativity = 0, .group = "4" },
|
||||
{ .symbol = "MC", .name = "Moscovium", .year_discovered = 2003, .atomic_mass = 29000, .electronegativity = 0, .group = "5" },
|
||||
{ .symbol = "LW", .name = "Livermorium", .year_discovered = 2000, .atomic_mass = 29300, .electronegativity = 0, .group = "6" },
|
||||
{ .symbol = "TS", .name = "Tennessine", .year_discovered = 2009, .atomic_mass = 29400, .electronegativity = 0, .group = "7" },
|
||||
{ .symbol = "OG", .name = "Oganesson", .year_discovered = 2002, .atomic_mass = 29400, .electronegativity = 0, .group = "0" },
|
||||
};
|
||||
|
||||
static void _make_upper(char *string) {
|
||||
size_t i = 0;
|
||||
while(string[i] != 0) {
|
||||
if (string[i] >= 'a' && string[i] <= 'z')
|
||||
string[i]-=32; // 32 = 'a'-'A'
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
static void _display_element(periodic_state_t *state)
|
||||
{
|
||||
char buf[9];
|
||||
char ele[3];
|
||||
uint8_t atomic_num = state->atomic_num;
|
||||
strcpy(ele, table[atomic_num - 1].symbol);
|
||||
_make_upper(ele);
|
||||
sprintf(buf, "%2s%3d %-2s", table[atomic_num - 1].group, atomic_num, ele);
|
||||
watch_display_string(buf, 2);
|
||||
}
|
||||
|
||||
static void _display_atomic_mass(periodic_state_t *state)
|
||||
{
|
||||
char buf[11];
|
||||
uint16_t mass = table[state->atomic_num - 1].atomic_mass;
|
||||
uint16_t integer = mass / 100;
|
||||
uint16_t decimal = mass % 100;
|
||||
if (decimal == 0)
|
||||
sprintf(buf, "%-2s%2s%4d", table[state->atomic_num - 1].symbol, screen_name[state->mode], integer);
|
||||
else
|
||||
sprintf(buf, "%-2s%2s%3d_%.2d", table[state->atomic_num - 1].symbol, screen_name[state->mode], integer, decimal);
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void _display_year_discovered(periodic_state_t *state)
|
||||
{
|
||||
char buf[11];
|
||||
char year_buf[7];
|
||||
int16_t year = table[state->atomic_num - 1].year_discovered;
|
||||
if (abs(year) > 9999)
|
||||
sprintf(year_buf, "---- ");
|
||||
else
|
||||
sprintf(year_buf, "%4d ", abs(year));
|
||||
if (year < 0) {
|
||||
year_buf[4] = 'b';
|
||||
year_buf[5] = 'c';
|
||||
}
|
||||
sprintf(buf, "%-2s%-2s%s", table[state->atomic_num - 1].symbol, screen_name[state->mode], year_buf);
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void _display_name(periodic_state_t *state)
|
||||
{
|
||||
char buf[11];
|
||||
_text_looping = table[state->atomic_num - 1].name;
|
||||
_text_pos = 0;
|
||||
sprintf(buf, "%-2s%-2s%s", table[state->atomic_num - 1].symbol, screen_name[state->mode], table[state->atomic_num - 1].name);
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void _display_electronegativity(periodic_state_t *state)
|
||||
{
|
||||
char buf[11];
|
||||
uint16_t electronegativity = table[state->atomic_num - 1].electronegativity;
|
||||
uint16_t integer = electronegativity / 100;
|
||||
uint16_t decimal = electronegativity % 100;
|
||||
if (decimal == 0)
|
||||
sprintf(buf, "%-2s%2s%4d", table[state->atomic_num - 1].symbol, screen_name[state->mode], integer);
|
||||
else
|
||||
sprintf(buf, "%-2s%2s%3d_%.2d", table[state->atomic_num - 1].symbol, screen_name[state->mode], integer, decimal);
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void start_quick_cyc(void){
|
||||
_quick_ticks_running = true;
|
||||
movement_request_tick_frequency(FREQ_FAST);
|
||||
}
|
||||
|
||||
static void stop_quick_cyc(void){
|
||||
_quick_ticks_running = false;
|
||||
movement_request_tick_frequency(FREQ);
|
||||
}
|
||||
|
||||
static int16_t _loop_text(const char* text, int8_t curr_loc, uint8_t char_len){
|
||||
// if curr_loc, then use that many ticks as a delay before looping
|
||||
char buf[15];
|
||||
uint8_t next_pos;
|
||||
uint8_t text_len = strlen(text);
|
||||
uint8_t pos = 10 - char_len;
|
||||
if (curr_loc == -1) curr_loc = 0; // To avoid double-showing the 0
|
||||
if (char_len >= text_len || curr_loc < 0) {
|
||||
sprintf(buf, "%s", text);
|
||||
watch_display_string(buf, pos);
|
||||
if (curr_loc < 0) return ++curr_loc;
|
||||
return 0;
|
||||
}
|
||||
else if (curr_loc == (text_len + 1))
|
||||
curr_loc = 0;
|
||||
next_pos = curr_loc + 1;
|
||||
sprintf(buf, "%.6s %.6s", text + curr_loc, text);
|
||||
watch_display_string(buf, pos);
|
||||
return next_pos;
|
||||
}
|
||||
|
||||
static void _display_title(periodic_state_t *state){
|
||||
state->atomic_num = 0;
|
||||
watch_clear_colon();
|
||||
watch_clear_all_indicators();
|
||||
_text_looping = title_text;
|
||||
_text_pos = FREQ * -1;
|
||||
_text_pos = _loop_text(_text_looping, _text_pos, 5);
|
||||
}
|
||||
|
||||
static void _display_screen(periodic_state_t *state, bool should_sound){
|
||||
watch_clear_display();
|
||||
watch_clear_all_indicators();
|
||||
switch (state->mode)
|
||||
{
|
||||
case SCREEN_TITLE:
|
||||
_display_title(state);
|
||||
break;
|
||||
case SCREEN_ELEMENT:
|
||||
case SCREENS_COUNT:
|
||||
_display_element(state);
|
||||
break;
|
||||
case SCREEN_ATOMIC_MASS:
|
||||
_display_atomic_mass(state);
|
||||
break;
|
||||
case SCREEN_DISCOVER_YEAR:
|
||||
_display_year_discovered(state);
|
||||
break;
|
||||
case SCREEN_ELECTRONEGATIVITY:
|
||||
_display_electronegativity(state);
|
||||
break;
|
||||
case SCREEN_FULL_NAME:
|
||||
_display_name(state);
|
||||
break;
|
||||
}
|
||||
if (should_sound) watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
|
||||
}
|
||||
|
||||
static void _handle_forward(periodic_state_t *state, bool should_sound){
|
||||
state->atomic_num = (state->atomic_num % MAX_ELEMENT) + 1; // Wraps back to 1
|
||||
state->mode = SCREEN_ELEMENT;
|
||||
_display_screen(state, false);
|
||||
if (should_sound) watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
|
||||
}
|
||||
|
||||
static void _handle_backward(periodic_state_t *state, bool should_sound){
|
||||
if (state->atomic_num <= 1) state->atomic_num = MAX_ELEMENT;
|
||||
else state->atomic_num = state->atomic_num - 1;
|
||||
state->mode = SCREEN_ELEMENT;
|
||||
_display_screen(state, false);
|
||||
if (should_sound) watch_buzzer_play_note(BUZZER_NOTE_A6, 50);
|
||||
}
|
||||
|
||||
static void _handle_mode_still_pressed(periodic_state_t *state, bool should_sound) {
|
||||
if (_ts_ticks != 0){
|
||||
if (!watch_get_pin_level(BTN_MODE)) {
|
||||
_ts_ticks = 0;
|
||||
return;
|
||||
}
|
||||
else if (--_ts_ticks == 0){
|
||||
switch (state->mode)
|
||||
{
|
||||
case SCREEN_TITLE:
|
||||
movement_move_to_face(0);
|
||||
return;
|
||||
case SCREEN_ELEMENT:
|
||||
state->mode = SCREEN_TITLE;
|
||||
_display_screen(state, should_sound);
|
||||
break;
|
||||
default:
|
||||
state->mode = SCREEN_ELEMENT;
|
||||
_display_screen(state, should_sound);
|
||||
break;
|
||||
}
|
||||
_ts_ticks = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool periodic_face_loop(movement_event_t event, movement_settings_t *settings, void *context)
|
||||
{
|
||||
periodic_state_t *state = (periodic_state_t *)context;
|
||||
switch (event.event_type)
|
||||
{
|
||||
case EVENT_ACTIVATE:
|
||||
state->mode = SCREEN_TITLE;
|
||||
_display_screen(state, false);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (state->mode == SCREEN_TITLE) _text_pos = _loop_text(_text_looping, _text_pos, 5);
|
||||
else if (state->mode == SCREEN_FULL_NAME) _text_pos = _loop_text(_text_looping, _text_pos, 6);
|
||||
if (_quick_ticks_running) {
|
||||
if (watch_get_pin_level(BTN_LIGHT)) _handle_backward(state, false);
|
||||
else if (watch_get_pin_level(BTN_ALARM)) _handle_forward(state, false);
|
||||
else stop_quick_cyc();
|
||||
}
|
||||
|
||||
_handle_mode_still_pressed(state, settings->bit.button_should_sound);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
if (state->mode <= SCREEN_ELEMENT) {
|
||||
_handle_backward(state, settings->bit.button_should_sound);
|
||||
}
|
||||
else {
|
||||
state->mode = SCREEN_ELEMENT;
|
||||
_display_screen(state, settings->bit.button_should_sound);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
if (state->mode <= SCREEN_ELEMENT) {
|
||||
_handle_forward(state, settings->bit.button_should_sound);
|
||||
}
|
||||
else {
|
||||
state->mode = SCREEN_ELEMENT;
|
||||
_display_screen(state, settings->bit.button_should_sound);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (state->mode <= SCREEN_ELEMENT) {
|
||||
start_quick_cyc();
|
||||
_handle_forward(state, settings->bit.button_should_sound);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
if (state->mode <= SCREEN_ELEMENT) {
|
||||
start_quick_cyc();
|
||||
_handle_backward(state, settings->bit.button_should_sound);
|
||||
}
|
||||
else {
|
||||
movement_illuminate_led();
|
||||
}
|
||||
break;
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
if (state->mode == SCREEN_TITLE) movement_move_to_next_face();
|
||||
else {
|
||||
state->mode = (state->mode + 1) % SCREENS_COUNT;
|
||||
if (state->mode == SCREEN_TITLE)
|
||||
state->mode = (state->mode + 1) % SCREENS_COUNT;
|
||||
if (state->mode == SCREEN_ELEMENT){
|
||||
_display_screen(state, false);
|
||||
if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_A6, 50);
|
||||
}
|
||||
else
|
||||
_display_screen(state, settings->bit.button_should_sound);
|
||||
}
|
||||
break;
|
||||
case EVENT_MODE_LONG_PRESS:
|
||||
switch (state->mode)
|
||||
{
|
||||
case SCREEN_TITLE:
|
||||
movement_move_to_face(0);
|
||||
return true;
|
||||
case SCREEN_ELEMENT:
|
||||
state->mode = SCREEN_TITLE;
|
||||
_display_screen(state, settings->bit.button_should_sound);
|
||||
break;
|
||||
default:
|
||||
state->mode = SCREEN_ELEMENT;
|
||||
_display_screen(state, settings->bit.button_should_sound);
|
||||
break;
|
||||
}
|
||||
_ts_ticks = 2;
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
// Display title after timeout
|
||||
if (state->mode == SCREEN_TITLE) break;
|
||||
state->mode = SCREEN_TITLE;
|
||||
_display_screen(state, false);
|
||||
break;
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
// Display static title and tick animation during LE
|
||||
watch_display_string("Pd Table", 0);
|
||||
watch_start_tick_animation(500);
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void periodic_face_resign(movement_settings_t *settings, void *context)
|
||||
{
|
||||
(void)settings;
|
||||
(void)context;
|
||||
|
||||
// handle any cleanup before your watch face goes off-screen.
|
||||
}
|
89
movement/watch_faces/complication/periodic_face.h
Normal file
89
movement/watch_faces/complication/periodic_face.h
Normal file
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 PrimmR
|
||||
* Copyright (c) 2024 David Volovskiy
|
||||
*
|
||||
* 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 PERIODIC_FACE_H_
|
||||
#define PERIODIC_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* Periodic Table Face
|
||||
* Allows for viewing data of the Periodic Table on your wrist.
|
||||
* When looking at an element, it'll show you the atomic number on the center of the screen,
|
||||
* symbol on the right, and it's group on the top-right.
|
||||
* Pressing the mode button will cycle through the pages.
|
||||
* Page 1: Atomic Mass
|
||||
* Page 2: Year Discovered
|
||||
* Page 3: Electronegativity
|
||||
* Page 4: Full Name of the Element
|
||||
*
|
||||
* Controls:
|
||||
* Mode Press
|
||||
* On Title: Next Screen
|
||||
* Else: Cycle through info of an element
|
||||
* Mode Hold
|
||||
* On Title: First Screen
|
||||
* On Element Symbol Screen: Go to Title Screen
|
||||
* Else: Go to Symbol Screen of current element
|
||||
* If you are in a subscreen and just keep holding MODE, you will go through all of these menus without needing to depress.
|
||||
*
|
||||
* Light Press
|
||||
* On Title or Element Symbol Screen: Previous Element
|
||||
* Else: Display currenlt-selected element symbol page
|
||||
* Light Hold
|
||||
* On Title Screen or Element Symbol: Fast Cycle through Previous Elements
|
||||
* Else: Activate LED backlight
|
||||
*
|
||||
* Alarm Press
|
||||
* On Title or Element Symbol Screen: Next Element
|
||||
* Else: Display currenlt-selected element symbol page
|
||||
* Alarm Hold
|
||||
* On Title Screen or Element Symbol: Fast Cycle through Next Elements
|
||||
*/
|
||||
|
||||
#define MODE_VIEW 0
|
||||
#define MODE_SELECT 1
|
||||
|
||||
typedef struct {
|
||||
uint8_t atomic_num;
|
||||
uint8_t mode;
|
||||
uint8_t selection_index;
|
||||
} periodic_state_t;
|
||||
|
||||
void periodic_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void periodic_face_activate(movement_settings_t *settings, void *context);
|
||||
bool periodic_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void periodic_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define periodic_face ((const watch_face_t){ \
|
||||
periodic_face_setup, \
|
||||
periodic_face_activate, \
|
||||
periodic_face_loop, \
|
||||
periodic_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // PERIODIC_FACE_H_
|
||||
|
|
@ -228,6 +228,7 @@ static void _planetary_hours(movement_settings_t *settings, planetary_hours_stat
|
|||
uint8_t weekday, planet, planetary_hour;
|
||||
uint32_t current_hour_epoch;
|
||||
watch_date_time scratch_time;
|
||||
bool set_leading_zero = false;
|
||||
|
||||
// check if we have a location. If not, display error
|
||||
if ( state->no_location ) {
|
||||
|
@ -253,7 +254,7 @@ static void _planetary_hours(movement_settings_t *settings, planetary_hours_stat
|
|||
return;
|
||||
}
|
||||
|
||||
if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
|
||||
// roll over hour iterator
|
||||
if ( state->hour < 0 ) state->hour = 23;
|
||||
|
@ -313,6 +314,8 @@ static void _planetary_hours(movement_settings_t *settings, planetary_hours_stat
|
|||
}
|
||||
scratch_time.unit.hour %= 12;
|
||||
if (scratch_time.unit.hour == 0) scratch_time.unit.hour = 12;
|
||||
} else if (settings->bit.clock_24h_leading_zero && scratch_time.unit.hour < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
|
||||
// planetary ruler of the hour
|
||||
|
@ -328,6 +331,8 @@ static void _planetary_hours(movement_settings_t *settings, planetary_hours_stat
|
|||
|
||||
watch_set_colon();
|
||||
watch_display_string(buf, 0);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
|
||||
if ( state->ruler == 2 ) _planetary_icon(planet);
|
||||
}
|
||||
|
|
|
@ -206,6 +206,7 @@ static void _planetary_time(movement_event_t event, movement_settings_t *setting
|
|||
double night_hour_count = 0.0;
|
||||
uint8_t weekday, planet, planetary_hour;
|
||||
double hour_duration, current_hour, current_minute, current_second;
|
||||
bool set_leading_zero = false;
|
||||
|
||||
watch_set_colon();
|
||||
|
||||
|
@ -218,7 +219,7 @@ static void _planetary_time(movement_event_t event, movement_settings_t *setting
|
|||
return;
|
||||
}
|
||||
|
||||
if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
|
||||
// PM for night hours, otherwise the night hours are counted from 13
|
||||
if ( state->night ) {
|
||||
|
@ -246,6 +247,9 @@ static void _planetary_time(movement_event_t event, movement_settings_t *setting
|
|||
state->scratch.unit.minute = floor(current_minute);
|
||||
state->scratch.unit.second = (uint8_t)floor(current_second) % 60;
|
||||
|
||||
if (settings->bit.clock_mode_24h && settings->bit.clock_24h_leading_zero && state->scratch.unit.hour < 10)
|
||||
set_leading_zero = true;
|
||||
|
||||
// what weekday is it (0 - 6)
|
||||
weekday = watch_utility_get_iso8601_weekday_number(state->scratch.unit.year, state->scratch.unit.month, state->scratch.unit.day) - 1;
|
||||
|
||||
|
@ -263,6 +267,8 @@ static void _planetary_time(movement_event_t event, movement_settings_t *setting
|
|||
else sprintf(buf, "%s h%2d%02d%02d", ruler, state->scratch.unit.hour, state->scratch.unit.minute, state->scratch.unit.second);
|
||||
|
||||
watch_display_string(buf, 0);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
|
||||
if ( state->ruler == 2 ) _planetary_icon(planet);
|
||||
|
||||
|
|
335
movement/watch_faces/complication/simon_face.c
Normal file
335
movement/watch_faces/complication/simon_face.c
Normal file
|
@ -0,0 +1,335 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 <#author_name#>
|
||||
*
|
||||
* 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 "simon_face.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// Emulator only: need time() to seed the random number generator
|
||||
#if __EMSCRIPTEN__
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
static char _simon_display_buf[12];
|
||||
static uint8_t _timer;
|
||||
static uint16_t _delay_beep;
|
||||
static uint16_t _timeout;
|
||||
static uint8_t _secSub;
|
||||
|
||||
static inline uint8_t _simon_get_rand_num(uint8_t num_values) {
|
||||
#if __EMSCRIPTEN__
|
||||
return rand() % num_values;
|
||||
#else
|
||||
return arc4random_uniform(num_values);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void _simon_clear_display(simon_state_t *state) {
|
||||
if (state->playing_state == SIMON_NOT_PLAYING) {
|
||||
watch_display_string(" ", 0);
|
||||
} else {
|
||||
sprintf(_simon_display_buf, " %2d ", state->sequence_length);
|
||||
watch_display_string(_simon_display_buf, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void _simon_not_playing_display(simon_state_t *state) {
|
||||
_simon_clear_display(state);
|
||||
|
||||
sprintf(_simon_display_buf, "SI %d", state->best_score);
|
||||
if (!state->soundOff)
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
else
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
if (!state->lightOff)
|
||||
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
else
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
watch_display_string(_simon_display_buf, 0);
|
||||
switch (state->mode)
|
||||
{
|
||||
case SIMON_MODE_EASY:
|
||||
watch_display_string("E", 9);
|
||||
break;
|
||||
case SIMON_MODE_HARD:
|
||||
watch_display_string("H", 9);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _simon_reset(simon_state_t *state) {
|
||||
state->playing_state = SIMON_NOT_PLAYING;
|
||||
state->listen_index = 0;
|
||||
state->sequence_length = 0;
|
||||
_simon_not_playing_display(state);
|
||||
}
|
||||
|
||||
|
||||
static void _simon_display_note(SimonNote note, simon_state_t *state) {
|
||||
char *ndtemplate = NULL;
|
||||
|
||||
switch (note) {
|
||||
case SIMON_LED_NOTE:
|
||||
ndtemplate = "LI%2d ";
|
||||
break;
|
||||
case SIMON_ALARM_NOTE:
|
||||
ndtemplate = " %2d AL";
|
||||
break;
|
||||
case SIMON_MODE_NOTE:
|
||||
ndtemplate = " %2dDE ";
|
||||
break;
|
||||
case SIMON_WRONG_NOTE:
|
||||
ndtemplate = "OH NOOOOO";
|
||||
}
|
||||
|
||||
sprintf(_simon_display_buf, ndtemplate, state->sequence_length);
|
||||
watch_display_string(_simon_display_buf, 0);
|
||||
}
|
||||
|
||||
static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_rest) {
|
||||
_simon_display_note(note, state);
|
||||
switch (note) {
|
||||
case SIMON_LED_NOTE:
|
||||
if (!state->lightOff) watch_set_led_yellow();
|
||||
if (state->soundOff)
|
||||
delay_ms(_delay_beep);
|
||||
else
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
|
||||
break;
|
||||
case SIMON_MODE_NOTE:
|
||||
if (!state->lightOff) watch_set_led_red();
|
||||
if (state->soundOff)
|
||||
delay_ms(_delay_beep);
|
||||
else
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep);
|
||||
break;
|
||||
case SIMON_ALARM_NOTE:
|
||||
if (!state->lightOff) watch_set_led_green();
|
||||
if (state->soundOff)
|
||||
delay_ms(_delay_beep);
|
||||
else
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep);
|
||||
break;
|
||||
case SIMON_WRONG_NOTE:
|
||||
if (state->soundOff)
|
||||
delay_ms(800);
|
||||
else
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A1, 800);
|
||||
break;
|
||||
}
|
||||
watch_set_led_off();
|
||||
|
||||
if (note != SIMON_WRONG_NOTE) {
|
||||
_simon_clear_display(state);
|
||||
if (!skip_rest) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_REST, (_delay_beep * 2)/3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void _simon_setup_next_note(simon_state_t *state) {
|
||||
if (state->sequence_length > state->best_score) {
|
||||
state->best_score = state->sequence_length;
|
||||
}
|
||||
|
||||
_simon_clear_display(state);
|
||||
state->playing_state = SIMON_TEACHING;
|
||||
state->sequence[state->sequence_length] = _simon_get_rand_num(3) + 1;
|
||||
state->sequence_length = state->sequence_length + 1;
|
||||
state->teaching_index = 0;
|
||||
state->listen_index = 0;
|
||||
}
|
||||
|
||||
static void _simon_listen(SimonNote note, simon_state_t *state) {
|
||||
if (state->sequence[state->listen_index] == note) {
|
||||
_simon_play_note(note, state, true);
|
||||
state->listen_index++;
|
||||
_timer = 0;
|
||||
|
||||
if (state->listen_index == state->sequence_length) {
|
||||
state->playing_state = SIMON_READY_FOR_NEXT_NOTE;
|
||||
}
|
||||
} else {
|
||||
_simon_play_note(SIMON_WRONG_NOTE, state, true);
|
||||
_simon_reset(state);
|
||||
}
|
||||
}
|
||||
|
||||
static void _simon_begin_listening(simon_state_t *state) {
|
||||
state->playing_state = SIMON_LISTENING_BACK;
|
||||
state->listen_index = 0;
|
||||
}
|
||||
|
||||
static void _simon_change_speed(simon_state_t *state){
|
||||
switch (state->mode)
|
||||
{
|
||||
case SIMON_MODE_HARD:
|
||||
_delay_beep = DELAY_FOR_TONE_MS / 2;
|
||||
_secSub = SIMON_FACE_FREQUENCY / 2;
|
||||
_timeout = (TIMER_MAX * SIMON_FACE_FREQUENCY) / 2;
|
||||
break;
|
||||
default:
|
||||
_delay_beep = DELAY_FOR_TONE_MS;
|
||||
_secSub = SIMON_FACE_FREQUENCY;
|
||||
_timeout = TIMER_MAX * SIMON_FACE_FREQUENCY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void simon_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(simon_state_t));
|
||||
memset(*context_ptr, 0, sizeof(simon_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.
|
||||
#if __EMSCRIPTEN__
|
||||
// simulator only: seed the randon number generator
|
||||
time_t t;
|
||||
srand((unsigned)time(&t));
|
||||
#endif
|
||||
}
|
||||
|
||||
void simon_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
simon_state_t *state = (simon_state_t *)context;
|
||||
_simon_change_speed(state);
|
||||
movement_request_tick_frequency(SIMON_FACE_FREQUENCY);
|
||||
_timer = 0;
|
||||
}
|
||||
|
||||
bool simon_face_loop(movement_event_t event, movement_settings_t *settings,
|
||||
void *context) {
|
||||
simon_state_t *state = (simon_state_t *)context;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
// Show your initial UI here.
|
||||
_simon_reset(state);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (state->playing_state == SIMON_LISTENING_BACK && state->mode != SIMON_MODE_EASY)
|
||||
{
|
||||
_timer++;
|
||||
if(_timer >= (_timeout)){
|
||||
_timer = 0;
|
||||
_simon_play_note(SIMON_WRONG_NOTE, state, true);
|
||||
_simon_reset(state);
|
||||
}
|
||||
}
|
||||
else if (state->playing_state == SIMON_TEACHING && event.subsecond == 0) {
|
||||
SimonNote note = state->sequence[state->teaching_index];
|
||||
// if this is the final note in the sequence, don't play the rest to let
|
||||
// the player jump in faster
|
||||
_simon_play_note(note, state, state->teaching_index == (state->sequence_length - 1));
|
||||
state->teaching_index++;
|
||||
|
||||
if (state->teaching_index == state->sequence_length) {
|
||||
_simon_begin_listening(state);
|
||||
}
|
||||
}
|
||||
else if (state->playing_state == SIMON_READY_FOR_NEXT_NOTE && (event.subsecond % _secSub) == 0) {
|
||||
_timer = 0;
|
||||
_simon_setup_next_note(state);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
if (state->playing_state == SIMON_NOT_PLAYING) {
|
||||
state->lightOff = !state->lightOff;
|
||||
_simon_not_playing_display(state);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (state->playing_state == SIMON_NOT_PLAYING) {
|
||||
state->soundOff = !state->soundOff;
|
||||
_simon_not_playing_display(state);
|
||||
if (!state->soundOff)
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
if (state->playing_state == SIMON_NOT_PLAYING) {
|
||||
state->sequence_length = 0;
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
_simon_setup_next_note(state);
|
||||
} else if (state->playing_state == SIMON_LISTENING_BACK) {
|
||||
_simon_listen(SIMON_LED_NOTE, state);
|
||||
}
|
||||
break;
|
||||
case EVENT_MODE_LONG_PRESS:
|
||||
if (state->playing_state == SIMON_NOT_PLAYING) {
|
||||
movement_move_to_face(0);
|
||||
} else {
|
||||
state->playing_state = SIMON_NOT_PLAYING;
|
||||
_simon_reset(state);
|
||||
}
|
||||
break;
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
if (state->playing_state == SIMON_NOT_PLAYING) {
|
||||
movement_move_to_next_face();
|
||||
} else if (state->playing_state == SIMON_LISTENING_BACK) {
|
||||
_simon_listen(SIMON_MODE_NOTE, state);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
if (state->playing_state == SIMON_LISTENING_BACK) {
|
||||
_simon_listen(SIMON_ALARM_NOTE, state);
|
||||
}
|
||||
else if (state->playing_state == SIMON_NOT_PLAYING){
|
||||
state->mode = (state->mode + 1) % SIMON_MODE_TOTAL;
|
||||
_simon_change_speed(state);
|
||||
_simon_not_playing_display(state);
|
||||
}
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
movement_move_to_face(0);
|
||||
break;
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void simon_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void)settings;
|
||||
(void)context;
|
||||
watch_set_led_off();
|
||||
watch_set_buzzer_off();
|
||||
}
|
111
movement/watch_faces/complication/simon_face.h
Normal file
111
movement/watch_faces/complication/simon_face.h
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 <#author_name#>
|
||||
*
|
||||
* 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 SIMON_FACE_H_
|
||||
#define SIMON_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* simon_face
|
||||
* -----------
|
||||
* The classic electronic game, Simon, reduced to be played on a Sensor-Watch
|
||||
*
|
||||
* How to play:
|
||||
*
|
||||
* When first arriving at the face, it will show your best score.
|
||||
*
|
||||
* Press the light button to start the game.
|
||||
*
|
||||
* A sequence will be played, starting with length 1. The sequence can be
|
||||
* made up of tones corresponding to any of the three buttons.
|
||||
*
|
||||
* light button: "LI" will display at the top of the screen, the LED will be yellow, and a high D will play
|
||||
* mode button: "DE" will display at the left of the screen, the LED will be red, and a high E will play
|
||||
* alarm button: "AL" will display on the right of the screen, the LED will be green, and a high C will play
|
||||
*
|
||||
* Once the sequence has finished, press the same buttons to recreate the sequence.
|
||||
*
|
||||
* If correct, the sequence will get one tone longer and play again. See how long of a sequence you can get.
|
||||
*
|
||||
* If you recreate the sequence incorrectly, a low note will play with "OH NOOOOO" displayed and the game is over.
|
||||
* Press light to play again.
|
||||
*
|
||||
* Once playing, long press the mode button when it is your turn to exit the game early.
|
||||
*/
|
||||
|
||||
#define MAX_SEQUENCE 99
|
||||
|
||||
typedef enum SimonNote {
|
||||
SIMON_LED_NOTE = 1,
|
||||
SIMON_MODE_NOTE,
|
||||
SIMON_ALARM_NOTE,
|
||||
SIMON_WRONG_NOTE
|
||||
} SimonNote;
|
||||
|
||||
typedef enum SimonPlayingState {
|
||||
SIMON_NOT_PLAYING = 0,
|
||||
SIMON_TEACHING,
|
||||
SIMON_LISTENING_BACK,
|
||||
SIMON_READY_FOR_NEXT_NOTE
|
||||
} SimonPlayingState;
|
||||
|
||||
typedef enum SimonMode {
|
||||
SIMON_MODE_NORMAL = 0, // 5 Second timeout if nothing is input
|
||||
SIMON_MODE_EASY, // There is no timeout in this mode
|
||||
SIMON_MODE_HARD, // The speed of the teaching is doubled and th etimeout is halved
|
||||
SIMON_MODE_TOTAL
|
||||
} SimonMode;
|
||||
|
||||
typedef struct {
|
||||
uint8_t best_score;
|
||||
SimonNote sequence[MAX_SEQUENCE];
|
||||
uint8_t sequence_length;
|
||||
uint8_t teaching_index;
|
||||
uint8_t listen_index;
|
||||
bool soundOff;
|
||||
bool lightOff;
|
||||
uint8_t mode:6;
|
||||
SimonPlayingState playing_state;
|
||||
} simon_state_t;
|
||||
|
||||
void simon_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr);
|
||||
void simon_face_activate(movement_settings_t *settings, void *context);
|
||||
bool simon_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void simon_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define simon_face \
|
||||
((const watch_face_t){ \
|
||||
simon_face_setup, \
|
||||
simon_face_activate, \
|
||||
simon_face_loop, \
|
||||
simon_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#define TIMER_MAX 5
|
||||
#define SIMON_FACE_FREQUENCY 8
|
||||
#define DELAY_FOR_TONE_MS 300
|
||||
|
||||
#endif // SIMON_FACE_H_
|
465
movement/watch_faces/complication/simple_calculator_face.c
Normal file
465
movement/watch_faces/complication/simple_calculator_face.c
Normal file
|
@ -0,0 +1,465 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Patrick McGuire
|
||||
*
|
||||
* 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 "simple_calculator_face.h"
|
||||
|
||||
void simple_calculator_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_calculator_state_t));
|
||||
memset(*context_ptr, 0, sizeof(simple_calculator_state_t));
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_to_zero(calculator_number_t *number) {
|
||||
number->negative = false;
|
||||
number->hundredths = 0;
|
||||
number->tenths = 0;
|
||||
number->ones = 0;
|
||||
number->tens = 0;
|
||||
number->hundreds = 0;
|
||||
number->thousands = 0;
|
||||
}
|
||||
|
||||
void simple_calculator_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
simple_calculator_state_t *state = (simple_calculator_state_t *)context;
|
||||
state->placeholder = PLACEHOLDER_ONES;
|
||||
state->mode = MODE_ENTERING_FIRST_NUM;
|
||||
reset_to_zero(&state->second_num);
|
||||
reset_to_zero(&state->result);
|
||||
movement_request_tick_frequency(4);
|
||||
}
|
||||
|
||||
static void increment_placeholder(calculator_number_t *number, calculator_placeholder_t placeholder) {
|
||||
uint8_t *digits[] = {
|
||||
&number->hundredths,
|
||||
&number->tenths,
|
||||
&number->ones,
|
||||
&number->tens,
|
||||
&number->hundreds,
|
||||
&number->thousands
|
||||
};
|
||||
*digits[placeholder] = (*digits[placeholder] + 1) % 10;
|
||||
}
|
||||
|
||||
static float convert_to_float(calculator_number_t number) {
|
||||
float result = 0.0;
|
||||
|
||||
// Add the whole number portion
|
||||
result += number.thousands * 1000.0f;
|
||||
result += number.hundreds * 100.0f;
|
||||
result += number.tens * 10.0f;
|
||||
result += number.ones * 1.0f;
|
||||
|
||||
// Add the fractional portion
|
||||
result += number.tenths * 0.1f;
|
||||
result += number.hundredths * 0.01f;
|
||||
|
||||
// Round to nearest hundredth
|
||||
result = roundf(result * 100) / 100;
|
||||
|
||||
// Handle negative numbers
|
||||
if (number.negative) result = -result;
|
||||
//printf("convert_to_float results = %f\n", result); // For debugging
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static char* update_display_number(calculator_number_t *number, char *display_string, uint8_t which_num) {
|
||||
char sign = ' ';
|
||||
if (number->negative) sign = '-';
|
||||
|
||||
sprintf(display_string, "CA%d%c%d%d%d%d%d%d",
|
||||
which_num,
|
||||
sign,
|
||||
number->thousands,
|
||||
number->hundreds,
|
||||
number->tens,
|
||||
number->ones,
|
||||
number->tenths,
|
||||
number->hundredths);
|
||||
|
||||
return display_string;
|
||||
}
|
||||
|
||||
static void set_operation(simple_calculator_state_t *state) {
|
||||
switch (state->operation) {
|
||||
case OP_ADD:
|
||||
watch_display_string(" Add", 0);
|
||||
break;
|
||||
case OP_SUB:
|
||||
watch_display_string(" sub", 0);
|
||||
break;
|
||||
case OP_MULT:
|
||||
watch_display_string(" n&ul", 0);
|
||||
break;
|
||||
case OP_DIV:
|
||||
watch_display_string(" div", 0);
|
||||
break;
|
||||
case OP_ROOT:
|
||||
watch_display_string(" root", 0);
|
||||
break;
|
||||
case OP_POWER:
|
||||
watch_display_string(" pow", 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void cycle_operation(simple_calculator_state_t *state) {
|
||||
state->operation = (state->operation + 1) % OPERATIONS_COUNT; // Assuming there are 6 operations
|
||||
}
|
||||
|
||||
|
||||
static calculator_number_t convert_to_string(float number) {
|
||||
calculator_number_t result;
|
||||
|
||||
// Handle negative numbers
|
||||
if (number < 0) {
|
||||
number = -number;
|
||||
result.negative = true;
|
||||
} else result.negative = false;
|
||||
|
||||
// Get each digit from each placeholder
|
||||
int int_part = (int)number;
|
||||
|
||||
float decimal_part_float = ((number - int_part) * 100); // two decimal places
|
||||
//printf("decimal_part_float = %f\n", decimal_part_float); //For debugging
|
||||
|
||||
int decimal_part = round(decimal_part_float);
|
||||
//printf("decimal_part = %d\n", decimal_part); //For debugging
|
||||
|
||||
result.thousands = int_part / 1000 % 10;
|
||||
result.hundreds = int_part / 100 % 10;
|
||||
result.tens = int_part / 10 % 10;
|
||||
result.ones = int_part % 10;
|
||||
|
||||
result.tenths = decimal_part / 10 % 10;
|
||||
result.hundredths = decimal_part % 10;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// This is the main function for setting the first_num and second_num
|
||||
// WISH: there must be a way to pass less to this function?
|
||||
static void set_number(calculator_number_t *number, calculator_placeholder_t placeholder, char *display_string, char *temp_display_string, movement_event_t event, uint8_t which_num) {
|
||||
|
||||
// Create the display index
|
||||
uint8_t display_index;
|
||||
|
||||
// Update display string with current number and copy into temp string
|
||||
update_display_number(number, display_string, which_num);
|
||||
strcpy(temp_display_string, display_string);
|
||||
|
||||
// Determine the display index based on the placeholder
|
||||
display_index = 9 - placeholder;
|
||||
|
||||
// Blink selected placeholder
|
||||
// Check if `event.subsecond` is even
|
||||
if (event.subsecond % 2 == 0) {
|
||||
// Replace the character at the index corresponding to the current placeholder with a space
|
||||
temp_display_string[display_index] = ' ';
|
||||
}
|
||||
|
||||
// Display the (possibly modified) string
|
||||
watch_display_string(temp_display_string, 0);
|
||||
}
|
||||
|
||||
static void view_results(simple_calculator_state_t *state, char *display_string) {
|
||||
|
||||
// Initialize float variables to do the math
|
||||
float first_num_float, second_num_float, result_float = 0.0f;
|
||||
|
||||
// Convert the passed numbers to floats
|
||||
first_num_float = convert_to_float(state->first_num);
|
||||
second_num_float = convert_to_float(state->second_num);
|
||||
|
||||
// Perform the calculation based on the selected operation
|
||||
switch (state->operation) {
|
||||
case OP_ADD:
|
||||
result_float = first_num_float + second_num_float;
|
||||
break;
|
||||
case OP_SUB:
|
||||
result_float = first_num_float - second_num_float;
|
||||
break;
|
||||
case OP_MULT:
|
||||
result_float = first_num_float * second_num_float;
|
||||
break;
|
||||
case OP_DIV:
|
||||
if (second_num_float != 0) {
|
||||
result_float = first_num_float / second_num_float;
|
||||
} else {
|
||||
state->mode = MODE_ERROR;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case OP_ROOT:
|
||||
if (first_num_float >= 0) {
|
||||
result_float = sqrtf(first_num_float);
|
||||
} else {
|
||||
state->mode = MODE_ERROR;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case OP_POWER:
|
||||
result_float = powf(first_num_float, second_num_float);
|
||||
break;
|
||||
default:
|
||||
result_float = 0.0f;
|
||||
break;
|
||||
}
|
||||
|
||||
// Be sure the result can fit on the watch display, else error
|
||||
if (result_float > 9999.99 || result_float < -9999.99) {
|
||||
state->mode = MODE_ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
result_float = roundf(result_float * 100.0f) / 100.0f; // Might not be needed
|
||||
|
||||
//printf("result as float = %f\n", result_float); // For debugging
|
||||
|
||||
// Convert the float result to a string
|
||||
// This isn't strictly necessary, but allows easily reusing the result as
|
||||
// the next calculation's first_num
|
||||
state->result = convert_to_string(result_float);
|
||||
|
||||
// Update the display with the result
|
||||
update_display_number(&state->result, display_string, 3);
|
||||
|
||||
//printf("display_string = %s\n", display_string); // For debugging
|
||||
|
||||
watch_display_string(display_string, 0);
|
||||
}
|
||||
|
||||
// Used both when returning from errors and when long pressing MODE
|
||||
static void reset_all(simple_calculator_state_t *state) {
|
||||
reset_to_zero(&state->first_num);
|
||||
reset_to_zero(&state->second_num);
|
||||
state->mode = MODE_ENTERING_FIRST_NUM;
|
||||
state->operation = OP_ADD;
|
||||
state->placeholder = PLACEHOLDER_ONES;
|
||||
}
|
||||
|
||||
bool simple_calculator_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
simple_calculator_state_t *state = (simple_calculator_state_t *)context;
|
||||
char display_string[10];
|
||||
char temp_display_string[10]; // Temporary buffer for blinking effect
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
case EVENT_TICK:
|
||||
switch (state->mode) {
|
||||
case MODE_ENTERING_FIRST_NUM:
|
||||
// See the WISH for this function above
|
||||
set_number(&state->first_num,
|
||||
state->placeholder,
|
||||
display_string,
|
||||
temp_display_string,
|
||||
event,
|
||||
1);
|
||||
break;
|
||||
|
||||
case MODE_CHOOSING:
|
||||
set_operation(state);
|
||||
break;
|
||||
|
||||
case MODE_ENTERING_SECOND_NUM:
|
||||
// If doing a square root calculation, skip to results
|
||||
if (state->operation == OP_ROOT) {
|
||||
state->mode = MODE_VIEW_RESULTS;
|
||||
} else {
|
||||
// See the WISH for this function above
|
||||
set_number(&state->second_num,
|
||||
state->placeholder,
|
||||
display_string,
|
||||
temp_display_string,
|
||||
event,
|
||||
2);
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_VIEW_RESULTS:
|
||||
view_results(state, display_string);
|
||||
break;
|
||||
|
||||
case MODE_ERROR:
|
||||
watch_display_string("CA Error ", 0);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
break;
|
||||
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
switch (state->mode) {
|
||||
case MODE_ENTERING_FIRST_NUM:
|
||||
case MODE_ENTERING_SECOND_NUM:
|
||||
// Move to the next placeholder when the light button is pressed
|
||||
state->placeholder = (state->placeholder + 1) % MAX_PLACEHOLDERS; // Loop back to the start after PLACEHOLDER_THOUSANDS
|
||||
break;
|
||||
case MODE_CHOOSING:
|
||||
cycle_operation(state);
|
||||
break;
|
||||
case MODE_ERROR:
|
||||
reset_all(state);
|
||||
break;
|
||||
case MODE_VIEW_RESULTS:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
switch (state->mode) {
|
||||
case MODE_ENTERING_FIRST_NUM:
|
||||
// toggle negative on state->first_num
|
||||
state->first_num.negative = !state->first_num.negative;
|
||||
break;
|
||||
case MODE_ENTERING_SECOND_NUM:
|
||||
// toggle negative on state->second_num
|
||||
state->second_num.negative = !state->second_num.negative;
|
||||
break;
|
||||
case MODE_ERROR:
|
||||
reset_all(state);
|
||||
break;
|
||||
case MODE_CHOOSING:
|
||||
case MODE_VIEW_RESULTS:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
switch (state->mode) {
|
||||
case MODE_ENTERING_FIRST_NUM:
|
||||
// Increment the digit in the current placeholder
|
||||
increment_placeholder(&state->first_num, state->placeholder);
|
||||
update_display_number(&state->first_num, display_string, 1);
|
||||
|
||||
//printf("display_string = %s\n", display_string); // For debugging
|
||||
|
||||
break;
|
||||
case MODE_CHOOSING:
|
||||
// Confirm and select the current operation
|
||||
state->mode = MODE_ENTERING_SECOND_NUM;
|
||||
break;
|
||||
case MODE_ENTERING_SECOND_NUM:
|
||||
// Increment the digit in the current placeholder
|
||||
increment_placeholder(&state->second_num, state->placeholder);
|
||||
update_display_number(&state->second_num, display_string, 2);
|
||||
|
||||
//printf("display_string = %s\n", display_string); // For debugging
|
||||
|
||||
break;
|
||||
case MODE_ERROR:
|
||||
reset_all(state);
|
||||
break;
|
||||
case MODE_VIEW_RESULTS:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
switch (state->mode) {
|
||||
case MODE_ENTERING_FIRST_NUM:
|
||||
reset_to_zero(&state->first_num);
|
||||
break;
|
||||
case MODE_ENTERING_SECOND_NUM:
|
||||
reset_to_zero(&state->second_num);
|
||||
break;
|
||||
case MODE_ERROR:
|
||||
reset_all(state);
|
||||
break;
|
||||
case MODE_CHOOSING:
|
||||
case MODE_VIEW_RESULTS:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EVENT_MODE_BUTTON_DOWN:
|
||||
break;
|
||||
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
if (state->mode == MODE_ERROR) {
|
||||
reset_all(state);
|
||||
} else if (state->mode == MODE_ENTERING_FIRST_NUM &&
|
||||
state->first_num.hundredths == 0 &&
|
||||
state->first_num.tenths == 0 &&
|
||||
state->first_num.ones== 0 &&
|
||||
state->first_num.tens == 0 &&
|
||||
state->first_num.hundreds == 0 &&
|
||||
state->first_num.thousands == 0) {
|
||||
movement_move_to_next_face();
|
||||
} else {
|
||||
// Reset the placeholder and proceed to the next MODE
|
||||
state->placeholder = PLACEHOLDER_ONES;
|
||||
state->mode = (state->mode + 1) % 4;
|
||||
// When looping back to MODE_ENTERING_FIRST_NUM, reuse the
|
||||
// previous calculation's results as the next calculation's
|
||||
// first_num; also reset other numbers
|
||||
if (state->mode == MODE_ENTERING_FIRST_NUM) {
|
||||
state->first_num = state->result;
|
||||
reset_to_zero(&state->second_num);
|
||||
reset_to_zero(&state->result);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EVENT_MODE_LONG_PRESS:
|
||||
// Move to next face if first number is 0
|
||||
if (state->first_num.hundredths == 0 &&
|
||||
state->first_num.tenths == 0 &&
|
||||
state->first_num.ones== 0 &&
|
||||
state->first_num.tens == 0 &&
|
||||
state->first_num.hundreds == 0 &&
|
||||
state->first_num.thousands == 0) {
|
||||
movement_move_to_face(0);
|
||||
// otherwise, start over
|
||||
} else {
|
||||
reset_all(state);
|
||||
}
|
||||
break;
|
||||
|
||||
case EVENT_TIMEOUT:
|
||||
movement_request_tick_frequency(1);
|
||||
movement_move_to_face(0);
|
||||
break;
|
||||
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void simple_calculator_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
movement_request_tick_frequency(1);
|
||||
}
|
||||
|
145
movement/watch_faces/complication/simple_calculator_face.h
Normal file
145
movement/watch_faces/complication/simple_calculator_face.h
Normal file
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Patrick McGuire
|
||||
*
|
||||
* 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_CALCULATOR_FACE_H_
|
||||
#define SIMPLE_CALCULATOR_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* Simple Calculator
|
||||
*
|
||||
* How to use:
|
||||
*
|
||||
* Flow:
|
||||
* Enter first number -> Select operator -> Enter second number -> View Results
|
||||
*
|
||||
* How to read the display:
|
||||
* - "CA" is displayed at the top to tell you that you're in the CAlculator
|
||||
* - The top-right digit (1, 2, or 3) lets you know whether you're entering the
|
||||
* first number (1), entering the second number (2), or viewing the results (3).
|
||||
* - To the right of the top-right digit will show the number's sign. If the
|
||||
* number is negative, a "-" will be displayed, otherwise it is empty.
|
||||
* - The 4 large digits to the left are whole numbers and the 2 smaller digits
|
||||
* on the right are the tenths and hundredths decimal places.
|
||||
*
|
||||
* Entering the first number:
|
||||
* - Press ALARM to increment the selected (blinking) digit
|
||||
* - Press LIGHT to move to the next placeholder
|
||||
* - LONG PRESS the LIGHT button to toggle the number's sign to make it
|
||||
* negative
|
||||
* - LONG PRESS the ALARM button to reset the number to 0
|
||||
* - Press MODE to proceed to selecting the operator
|
||||
*
|
||||
* Selecting the operator:
|
||||
* - Press the LIGHT button to cycle through available operators. They are:
|
||||
* + Add
|
||||
* - Subtract
|
||||
* * Multiply
|
||||
* / Divide
|
||||
* sqrtf() Square root
|
||||
* powf() Power (exponent calculation)
|
||||
* - Press MODE or ALARM to proceed to entering the second number
|
||||
*
|
||||
* Entering the second number:
|
||||
* - Everything is the same as setting the first number except that pressing
|
||||
* MODE here will proceed to viewing the results
|
||||
*
|
||||
* Viewing the results:
|
||||
* - Pressing MODE will start a new calculation with the result as the first
|
||||
* number. (LONG PRESS ALARM to reset the value to 0)
|
||||
*
|
||||
* Errors:
|
||||
* - An error will be triggered if the result is not able to be displayed, that
|
||||
* is, if the value is greater than 9,999.99 or less than -9,999.99.
|
||||
* - An error will also be triggered if an impossible operation is selected,
|
||||
* for instance trying to divide by 0 or get the square root of a negative
|
||||
* number.
|
||||
* - Exit error mode and start over with any button press.
|
||||
*
|
||||
*/
|
||||
|
||||
#define OPERATIONS_COUNT 6
|
||||
#define MAX_PLACEHOLDERS 6
|
||||
|
||||
typedef struct {
|
||||
bool negative;
|
||||
uint8_t hundredths;
|
||||
uint8_t tenths;
|
||||
uint8_t ones;
|
||||
uint8_t tens;
|
||||
uint8_t hundreds;
|
||||
uint8_t thousands;
|
||||
} calculator_number_t;
|
||||
|
||||
typedef enum {
|
||||
PLACEHOLDER_HUNDREDTHS,
|
||||
PLACEHOLDER_TENTHS,
|
||||
PLACEHOLDER_ONES,
|
||||
PLACEHOLDER_TENS,
|
||||
PLACEHOLDER_HUNDREDS,
|
||||
PLACEHOLDER_THOUSANDS
|
||||
} calculator_placeholder_t;
|
||||
|
||||
typedef enum {
|
||||
OP_ADD,
|
||||
OP_SUB,
|
||||
OP_MULT,
|
||||
OP_DIV,
|
||||
OP_ROOT,
|
||||
OP_POWER,
|
||||
} calculator_operation_t;
|
||||
|
||||
typedef enum {
|
||||
MODE_ENTERING_FIRST_NUM,
|
||||
MODE_CHOOSING,
|
||||
MODE_ENTERING_SECOND_NUM,
|
||||
MODE_VIEW_RESULTS,
|
||||
MODE_ERROR
|
||||
} calculator_mode_t;
|
||||
|
||||
typedef struct {
|
||||
calculator_number_t first_num;
|
||||
calculator_number_t second_num;
|
||||
calculator_number_t result;
|
||||
calculator_operation_t operation;
|
||||
calculator_mode_t mode;
|
||||
calculator_placeholder_t placeholder;
|
||||
} simple_calculator_state_t;
|
||||
|
||||
void simple_calculator_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void simple_calculator_face_activate(movement_settings_t *settings, void *context);
|
||||
bool simple_calculator_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void simple_calculator_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define simple_calculator_face ((const watch_face_t){ \
|
||||
simple_calculator_face_setup, \
|
||||
simple_calculator_face_activate, \
|
||||
simple_calculator_face_loop, \
|
||||
simple_calculator_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // SIMPLE_CALCULATOR_FACE_H_
|
||||
|
504
movement/watch_faces/complication/smallchess_face.c
Normal file
504
movement/watch_faces/complication/smallchess_face.c
Normal file
|
@ -0,0 +1,504 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Jeremy O'Brien
|
||||
*
|
||||
* 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 "smallchesslib.h"
|
||||
|
||||
#include "smallchess_face.h"
|
||||
#include "watch.h"
|
||||
|
||||
#define PIECE_LIST_END_MARKER 0xff
|
||||
|
||||
int8_t cpu_done_beep[] = {BUZZER_NOTE_C5, 5, BUZZER_NOTE_C6, 5, BUZZER_NOTE_C7, 5, 0};
|
||||
|
||||
static void smallchess_init_board(smallchess_face_state_t *state) {
|
||||
SCL_gameInit((SCL_Game *)state->game, 0);
|
||||
memset(state->moveable_pieces, 0xff, sizeof(state->moveable_pieces));
|
||||
memset(state->moveable_dests, 0xff, sizeof(state->moveable_dests));
|
||||
}
|
||||
|
||||
void smallchess_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(smallchess_face_state_t));
|
||||
memset(*context_ptr, 0, sizeof(smallchess_face_state_t));
|
||||
|
||||
/* now alloc/init the game board */
|
||||
smallchess_face_state_t *state = (smallchess_face_state_t *)*context_ptr;
|
||||
state->game = malloc(sizeof(SCL_Game));
|
||||
smallchess_init_board(*context_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
void smallchess_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
static void _smallchess_calc_moveable_pieces(smallchess_face_state_t *state) {
|
||||
int moveable_pieces_idx = 0;
|
||||
SCL_Game *game = (SCL_Game *)state->game;
|
||||
for (int i = 0; i < SCL_BOARD_SQUARES; ++i) {
|
||||
if (game->board[i] != '.' &&
|
||||
SCL_pieceIsWhite(game->board[i]) == SCL_boardWhitesTurn(game->board)) {
|
||||
SCL_SquareSet moveable_pieces = SCL_SQUARE_SET_EMPTY;
|
||||
SCL_boardGetMoves(game->board, i, moveable_pieces);
|
||||
if (SCL_squareSetSize(moveable_pieces) != 0) {
|
||||
state->moveable_pieces[moveable_pieces_idx] = i;
|
||||
moveable_pieces_idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
state->moveable_pieces[moveable_pieces_idx] = PIECE_LIST_END_MARKER;
|
||||
state->moveable_pieces_idx = 0;
|
||||
}
|
||||
|
||||
static void _smallchess_make_ai_move(smallchess_face_state_t *state) {
|
||||
char ai_from_str[3] = {0};
|
||||
char ai_to_str[3] = {0};
|
||||
uint8_t rep_from, rep_to;
|
||||
char ai_prom;
|
||||
|
||||
watch_clear_display();
|
||||
watch_start_character_blink('C', 100);
|
||||
SCL_gameGetRepetiotionMove(state->game, &rep_from, &rep_to);
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
hri_oscctrl_write_OSC16MCTRL_FSEL_bf(OSCCTRL, OSCCTRL_OSC16MCTRL_FSEL_16_Val);
|
||||
#endif
|
||||
SCL_getAIMove(state->game, 3, 0, 0, SCL_boardEvaluateStatic, NULL, 0, rep_from, rep_to, &state->ai_from_square, &state->ai_to_square, &ai_prom);
|
||||
#ifndef __EMSCRIPTEN__
|
||||
hri_oscctrl_write_OSC16MCTRL_FSEL_bf(OSCCTRL, OSCCTRL_OSC16MCTRL_FSEL_4_Val);
|
||||
#endif
|
||||
|
||||
SCL_gameMakeMove(state->game, state->ai_from_square, state->ai_to_square, ai_prom);
|
||||
watch_stop_blink();
|
||||
|
||||
watch_buzzer_play_sequence(cpu_done_beep, NULL);
|
||||
|
||||
/* cache the move as a string for SHOW_CPU_MOVE state */
|
||||
SCL_squareToString(state->ai_from_square, ai_from_str);
|
||||
SCL_squareToString(state->ai_to_square, ai_to_str);
|
||||
snprintf(state->last_move_str, sizeof(state->last_move_str), " %s-%s", ai_from_str, ai_to_str);
|
||||
|
||||
/* now cache the list of legal pieces we can move */
|
||||
_smallchess_calc_moveable_pieces(state);
|
||||
}
|
||||
|
||||
static char _smallchess_make_lowercase(char c) {
|
||||
if (c < 0x61)
|
||||
return c + 0x20;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
static void _smallchess_get_endgame_string(smallchess_face_state_t *state, char *buf, uint8_t len) {
|
||||
uint8_t endgame_state = ((SCL_Game *)state->game)->state;
|
||||
uint16_t ply = ((SCL_Game *)state->game)->ply;
|
||||
|
||||
switch (endgame_state) {
|
||||
case SCL_GAME_STATE_WHITE_WIN:
|
||||
snprintf(buf, len, "Wh%2dm&ate ", ply);
|
||||
break;
|
||||
case SCL_GAME_STATE_BLACK_WIN:
|
||||
snprintf(buf, len, "bL%2dm&ate ", ply);
|
||||
break;
|
||||
case SCL_GAME_STATE_DRAW:
|
||||
case SCL_GAME_STATE_DRAW_STALEMATE:
|
||||
case SCL_GAME_STATE_DRAW_REPETITION:
|
||||
case SCL_GAME_STATE_DRAW_50:
|
||||
case SCL_GAME_STATE_DRAW_DEAD:
|
||||
snprintf(buf, len, " %2d Drauu", ply);
|
||||
break;
|
||||
default:
|
||||
snprintf(buf, len, " %2d Error", ply);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_face_update_lcd(smallchess_face_state_t *state) {
|
||||
uint8_t start_square;
|
||||
uint8_t end_square;
|
||||
char start_coord[3] = {0};
|
||||
char end_coord[3] = {0};
|
||||
char buf[14] = {0};
|
||||
|
||||
uint16_t ply = ((SCL_Game *)state->game)->ply;
|
||||
|
||||
switch (state->state) {
|
||||
case SMALLCHESS_MENU_RESUME:
|
||||
snprintf(buf, sizeof(buf), "SC%2dResume", ply);
|
||||
break;
|
||||
case SMALLCHESS_MENU_UNDO:
|
||||
snprintf(buf, sizeof(buf), "SC%2d Undo ", ply);
|
||||
break;
|
||||
case SMALLCHESS_MENU_SHOW_LAST_MOVE:
|
||||
snprintf(buf, sizeof(buf), "SC%2dShLast", ply);
|
||||
break;
|
||||
case SMALLCHESS_MENU_NEW_WHITE:
|
||||
snprintf(buf, sizeof(buf), "Wh%2dStart ", ply);
|
||||
break;
|
||||
case SMALLCHESS_MENU_NEW_BLACK:
|
||||
snprintf(buf, sizeof(buf), "bL%2dStart ", ply);
|
||||
break;
|
||||
case SMALLCHESS_SHOW_CPU_MOVE:
|
||||
case SMALLCHESS_SHOW_LAST_MOVE:
|
||||
snprintf(buf,
|
||||
sizeof(buf),
|
||||
"%c %2d%s",
|
||||
_smallchess_make_lowercase(((SCL_Game *)state->game)->board[state->ai_to_square]),
|
||||
ply,
|
||||
state->last_move_str);
|
||||
|
||||
break;
|
||||
case SMALLCHESS_SELECT_PIECE:
|
||||
if (((SCL_Game *)state->game)->state != SCL_GAME_STATE_PLAYING) {
|
||||
_smallchess_get_endgame_string(state, buf, sizeof(buf));
|
||||
break;
|
||||
}
|
||||
start_square = state->moveable_pieces[state->moveable_pieces_idx];
|
||||
SCL_squareToString(start_square, start_coord);
|
||||
snprintf(buf,
|
||||
sizeof(buf),
|
||||
"%c %2d %s- ",
|
||||
_smallchess_make_lowercase(((SCL_Game *)state->game)->board[start_square]),
|
||||
ply + 1,
|
||||
start_coord);
|
||||
break;
|
||||
case SMALLCHESS_SELECT_DEST:
|
||||
start_square = state->moveable_pieces[state->moveable_pieces_idx];
|
||||
SCL_squareToString(start_square, start_coord);
|
||||
end_square = state->moveable_dests[state->moveable_dests_idx];
|
||||
SCL_squareToString(end_square, end_coord);
|
||||
snprintf(buf,
|
||||
sizeof(buf),
|
||||
"%c %2d %s-%s",
|
||||
_smallchess_make_lowercase(((SCL_Game *)state->game)->board[start_square]),
|
||||
ply + 1,
|
||||
start_coord,
|
||||
end_coord);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void _smallchess_select_main_menu_subitem(smallchess_face_state_t *state) {
|
||||
char from_str[3] = {0};
|
||||
char to_str[3] = {0};
|
||||
char prom;
|
||||
|
||||
switch (state->state) {
|
||||
case SMALLCHESS_MENU_RESUME:
|
||||
state->state = SMALLCHESS_SELECT_PIECE;
|
||||
break;
|
||||
case SMALLCHESS_MENU_UNDO:
|
||||
/* undo twice to undo the CPU's move and our move */
|
||||
SCL_gameUndoMove((SCL_Game *)state->game);
|
||||
SCL_gameUndoMove((SCL_Game *)state->game);
|
||||
/* and re-calculate the moveable pieces for this new state */
|
||||
_smallchess_calc_moveable_pieces(state);
|
||||
break;
|
||||
case SMALLCHESS_MENU_NEW_WHITE:
|
||||
SCL_gameInit((SCL_Game *)state->game, 0);
|
||||
_smallchess_calc_moveable_pieces(state);
|
||||
state->state = SMALLCHESS_SELECT_PIECE;
|
||||
break;
|
||||
case SMALLCHESS_MENU_NEW_BLACK:
|
||||
SCL_gameInit((SCL_Game *)state->game, 0);
|
||||
/* force a move since black is playing */
|
||||
_smallchess_make_ai_move(state);
|
||||
state->state = SMALLCHESS_SHOW_CPU_MOVE;
|
||||
break;
|
||||
case SMALLCHESS_MENU_SHOW_LAST_MOVE:
|
||||
/* fetch the move */
|
||||
SCL_recordGetMove(((SCL_Game *)state->game)->record, ((SCL_Game *)state->game)->ply - 1, &state->ai_from_square, &state->ai_to_square, &prom);
|
||||
SCL_squareToString(state->ai_from_square, from_str);
|
||||
SCL_squareToString(state->ai_to_square, to_str);
|
||||
snprintf(state->last_move_str, sizeof(state->last_move_str), " %s-%s", from_str, to_str);
|
||||
state->state = SMALLCHESS_SHOW_LAST_MOVE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_handle_select_piece_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
SCL_SquareSet moveable_dests = SCL_SQUARE_SET_EMPTY;
|
||||
|
||||
/* back to main menu on any event when game ends */
|
||||
if (((SCL_Game *)state->game)->state != SCL_GAME_STATE_PLAYING) {
|
||||
state->state = SMALLCHESS_MENU_RESUME;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
// check for no moves possible state (shouldn't happen but this will prevent weirdness)
|
||||
if (state->moveable_pieces[0] == PIECE_LIST_END_MARKER) {
|
||||
return;
|
||||
}
|
||||
|
||||
state->moveable_pieces_idx += 1;
|
||||
if (state->moveable_pieces_idx >= NUM_ELEMENTS(state->moveable_pieces)) {
|
||||
state->moveable_pieces_idx = 0;
|
||||
}
|
||||
|
||||
if (state->moveable_pieces[state->moveable_pieces_idx] == PIECE_LIST_END_MARKER) {
|
||||
state->moveable_pieces_idx = 0;
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
// check for no moves possible state (shouldn't happen but this will prevent weirdness)
|
||||
if (state->moveable_pieces[0] == PIECE_LIST_END_MARKER) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* handle wrap around */
|
||||
if (state->moveable_pieces_idx == 0) {
|
||||
for (unsigned int i = 0; i < NUM_ELEMENTS(state->moveable_pieces); i++) {
|
||||
if (state->moveable_pieces[i] == 0xff) {
|
||||
state->moveable_pieces_idx = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state->moveable_pieces_idx -= 1;
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
if (((SCL_Game *)state->game)->ply == 0) {
|
||||
state->state = SMALLCHESS_MENU_NEW_WHITE;
|
||||
} else {
|
||||
state->state = SMALLCHESS_MENU_RESUME;
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
/* pre-calculate the possible moves this piece can make */
|
||||
SCL_boardGetMoves(((SCL_Game *)state->game)->board, state->moveable_pieces[state->moveable_pieces_idx], moveable_dests);
|
||||
state->moveable_dests_idx = 0;
|
||||
SCL_SQUARE_SET_ITERATE_BEGIN(moveable_dests)
|
||||
state->moveable_dests[state->moveable_dests_idx] = iteratedSquare;
|
||||
state->moveable_dests_idx++;
|
||||
SCL_SQUARE_SET_ITERATE_END
|
||||
state->moveable_dests[state->moveable_dests_idx] = PIECE_LIST_END_MARKER;
|
||||
state->moveable_dests_idx = 0;
|
||||
state->state = SMALLCHESS_SELECT_DEST;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_handle_select_dest_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
// check for no moves possible state (shouldn't happen but this will prevent weirdness)
|
||||
if (state->moveable_dests[0] == PIECE_LIST_END_MARKER) {
|
||||
return;
|
||||
}
|
||||
state->moveable_dests_idx += 1;
|
||||
if (state->moveable_dests_idx >= (sizeof(state->moveable_dests) / sizeof(state->moveable_dests[0]))) {
|
||||
state->moveable_dests_idx = 0;
|
||||
}
|
||||
|
||||
if (state->moveable_dests[state->moveable_dests_idx] == PIECE_LIST_END_MARKER) {
|
||||
state->moveable_dests_idx = 0;
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
// check for no moves possible state (shouldn't happen but this will prevent weirdness)
|
||||
if (state->moveable_dests[0] == PIECE_LIST_END_MARKER) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* handle wrap around */
|
||||
if (state->moveable_dests_idx == 0) {
|
||||
for (unsigned int i = 0; i < NUM_ELEMENTS(state->moveable_dests); i++) {
|
||||
if (state->moveable_dests[i] == 0xff) {
|
||||
state->moveable_dests_idx = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state->moveable_dests_idx -= 1;
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
state->state = SMALLCHESS_SELECT_PIECE;
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
SCL_gameMakeMove((SCL_Game *)state->game, state->moveable_pieces[state->moveable_pieces_idx], state->moveable_dests[state->moveable_dests_idx], 'q');
|
||||
|
||||
/* if the player didn't win or draw here, calculate a move */
|
||||
if (((SCL_Game *)state->game)->state == SCL_GAME_STATE_PLAYING) {
|
||||
_smallchess_make_ai_move(state);
|
||||
state->state = SMALLCHESS_SHOW_CPU_MOVE;
|
||||
} else {
|
||||
/* player ended the game through mate or draw; jump to select piece screen to show state */
|
||||
state->state = SMALLCHESS_SELECT_PIECE;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* this just waits until any button is hit */
|
||||
static void _smallchess_handle_show_cpu_move_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
state->state = SMALLCHESS_SELECT_PIECE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_handle_show_last_move_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
state->state = SMALLCHESS_MENU_SHOW_LAST_MOVE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_handle_playing_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
if (state->state == SMALLCHESS_SELECT_PIECE) {
|
||||
_smallchess_handle_select_piece_button_event(state, event);
|
||||
} else if (state->state == SMALLCHESS_SELECT_DEST) {
|
||||
_smallchess_handle_select_dest_button_event(state, event);
|
||||
} else if (state->state == SMALLCHESS_SHOW_CPU_MOVE) {
|
||||
_smallchess_handle_show_cpu_move_button_event(state, event);
|
||||
} else if (state->state == SMALLCHESS_SHOW_LAST_MOVE) {
|
||||
_smallchess_handle_show_last_move_button_event(state, event);
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_handle_main_menu_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
uint16_t ply = ((SCL_Game *)state->game)->ply;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
/* no game started; only offer start white/start black */
|
||||
if (ply == 0) {
|
||||
if (state->state == SMALLCHESS_MENU_NEW_WHITE) {
|
||||
state->state = SMALLCHESS_MENU_NEW_BLACK;
|
||||
} else {
|
||||
state->state = SMALLCHESS_MENU_NEW_WHITE;
|
||||
}
|
||||
} else {
|
||||
state->state++;
|
||||
if (state->state >= SMALLCHESS_PLAYING_SPLIT) {
|
||||
state->state = SMALLCHESS_MENU_RESUME;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
/* no game started; only offer start white/start black */
|
||||
if (ply == 0) {
|
||||
if (state->state == SMALLCHESS_MENU_NEW_BLACK) {
|
||||
state->state = SMALLCHESS_MENU_NEW_WHITE;
|
||||
} else {
|
||||
state->state = SMALLCHESS_MENU_NEW_BLACK;
|
||||
}
|
||||
} else {
|
||||
if (state->state == SMALLCHESS_MENU_RESUME) {
|
||||
state->state = SMALLCHESS_PLAYING_SPLIT - 1;
|
||||
} else {
|
||||
state->state--;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
_smallchess_select_main_menu_subitem(state);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_handle_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
if (state->state < SMALLCHESS_PLAYING_SPLIT) {
|
||||
/* in main menu */
|
||||
_smallchess_handle_main_menu_button_event(state, event);
|
||||
} else if (state->state > SMALLCHESS_PLAYING_SPLIT) {
|
||||
/* in piece selection */
|
||||
_smallchess_handle_playing_button_event(state, event);
|
||||
}
|
||||
}
|
||||
|
||||
bool smallchess_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
smallchess_face_state_t *state = (smallchess_face_state_t *)context;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
if (((SCL_Game *)state->game)->ply == 0) {
|
||||
state->state = SMALLCHESS_MENU_NEW_WHITE;
|
||||
} else {
|
||||
state->state = SMALLCHESS_MENU_RESUME;
|
||||
}
|
||||
_smallchess_face_update_lcd(state);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
_smallchess_handle_button_event(state, event);
|
||||
_smallchess_face_update_lcd(state);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
break;
|
||||
default:
|
||||
movement_default_loop_handler(event, settings);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void smallchess_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
watch_set_led_off();
|
||||
}
|
90
movement/watch_faces/complication/smallchess_face.h
Normal file
90
movement/watch_faces/complication/smallchess_face.h
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Jeremy O'Brien
|
||||
*
|
||||
* 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 SMALLCHESS_FACE_H_
|
||||
#define SMALLCHESS_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* Chess watchface
|
||||
*
|
||||
* Implements a (very) simple chess engine.
|
||||
* Uses smallchesslib for the engine: https://codeberg.org/drummyfish/smallchesslib
|
||||
*
|
||||
* When moving a piece, only valid pieces and moves are presented.
|
||||
*
|
||||
* Interaction is done through a simple menu/submenu system:
|
||||
* - Light button: navigate backwards through the current menu
|
||||
* - Alarm button: navigate forwards through the current menu
|
||||
* - Light button (long press): navigate up to the parent menu
|
||||
* - Alarm button (long press): select the current item or submenu
|
||||
*/
|
||||
|
||||
enum smallchess_state {
|
||||
/* main menu */
|
||||
SMALLCHESS_MENU_RESUME,
|
||||
SMALLCHESS_MENU_SHOW_LAST_MOVE,
|
||||
SMALLCHESS_MENU_UNDO,
|
||||
SMALLCHESS_MENU_NEW_WHITE,
|
||||
SMALLCHESS_MENU_NEW_BLACK,
|
||||
|
||||
SMALLCHESS_PLAYING_SPLIT,
|
||||
|
||||
/* playing game submenu */
|
||||
SMALLCHESS_SHOW_LAST_MOVE,
|
||||
SMALLCHESS_SHOW_CPU_MOVE,
|
||||
SMALLCHESS_SELECT_PIECE,
|
||||
SMALLCHESS_SELECT_DEST,
|
||||
};
|
||||
|
||||
#define NUM_ELEMENTS(a) (sizeof(a) / sizeof(a[0]))
|
||||
#define SMALLCHESS_NUM_PIECES 16 // number of pieces each player has
|
||||
|
||||
typedef struct {
|
||||
void *game;
|
||||
enum smallchess_state state;
|
||||
uint8_t moveable_pieces[SMALLCHESS_NUM_PIECES + 1];
|
||||
uint8_t moveable_pieces_idx;
|
||||
uint8_t moveable_dests[29]; // this magic number represents the maximum number of moves a piece can make (queen in center of board)
|
||||
// plus one for the end list marker
|
||||
uint8_t moveable_dests_idx;
|
||||
char last_move_str[7];
|
||||
uint8_t ai_from_square, ai_to_square;
|
||||
} smallchess_face_state_t;
|
||||
|
||||
void smallchess_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void smallchess_face_activate(movement_settings_t *settings, void *context);
|
||||
bool smallchess_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void smallchess_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define smallchess_face ((const watch_face_t){ \
|
||||
smallchess_face_setup, \
|
||||
smallchess_face_activate, \
|
||||
smallchess_face_loop, \
|
||||
smallchess_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // SMALLCHESS_FACE_H_
|
|
@ -49,7 +49,7 @@ static void _sunrise_sunset_face_update(movement_settings_t *settings, sunrise_s
|
|||
double rise, set, minutes, seconds;
|
||||
bool show_next_match = false;
|
||||
movement_location_t movement_location;
|
||||
if (state->longLatToUse == 0)
|
||||
if (state->longLatToUse == 0 || _location_count <= 1)
|
||||
movement_location = (movement_location_t) watch_get_backup_data(1);
|
||||
else{
|
||||
movement_location.bit.latitude = longLatPresets[state->longLatToUse].latitude;
|
||||
|
@ -93,7 +93,7 @@ static void _sunrise_sunset_face_update(movement_settings_t *settings, sunrise_s
|
|||
}
|
||||
|
||||
watch_set_colon();
|
||||
if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
|
||||
rise += hours_from_utc;
|
||||
set += hours_from_utc;
|
||||
|
@ -113,12 +113,17 @@ static void _sunrise_sunset_face_update(movement_settings_t *settings, sunrise_s
|
|||
|
||||
if (date_time.reg < scratch_time.reg || show_next_match) {
|
||||
if (state->rise_index == 0 || show_next_match) {
|
||||
bool set_leading_zero = false;
|
||||
if (!settings->bit.clock_mode_24h) {
|
||||
if (watch_utility_convert_to_12_hour(&scratch_time)) watch_set_indicator(WATCH_INDICATOR_PM);
|
||||
else watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
} else if (settings->bit.clock_24h_leading_zero && scratch_time.unit.hour < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
sprintf(buf, "rI%2d%2d%02d%s", scratch_time.unit.day, scratch_time.unit.hour, scratch_time.unit.minute,longLatPresets[state->longLatToUse].name);
|
||||
watch_display_string(buf, 0);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
return;
|
||||
} else {
|
||||
show_next_match = true;
|
||||
|
@ -140,12 +145,17 @@ static void _sunrise_sunset_face_update(movement_settings_t *settings, sunrise_s
|
|||
|
||||
if (date_time.reg < scratch_time.reg || show_next_match) {
|
||||
if (state->rise_index == 0 || show_next_match) {
|
||||
bool set_leading_zero = false;
|
||||
if (!settings->bit.clock_mode_24h) {
|
||||
if (watch_utility_convert_to_12_hour(&scratch_time)) watch_set_indicator(WATCH_INDICATOR_PM);
|
||||
else watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
} else if (settings->bit.clock_24h_leading_zero && scratch_time.unit.hour < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
sprintf(buf, "SE%2d%2d%02d%s", scratch_time.unit.day, scratch_time.unit.hour, scratch_time.unit.minute, longLatPresets[state->longLatToUse].name);
|
||||
watch_display_string(buf, 0);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
return;
|
||||
} else {
|
||||
show_next_match = true;
|
||||
|
@ -359,7 +369,7 @@ bool sunrise_sunset_face_loop(movement_event_t event, movement_settings_t *setti
|
|||
_sunrise_sunset_face_update_location_register(state);
|
||||
}
|
||||
_sunrise_sunset_face_update_settings_display(event, context);
|
||||
} else if (_location_count == 1) {
|
||||
} else if (_location_count <= 1) {
|
||||
movement_illuminate_led();
|
||||
}
|
||||
if (state->page == 0) {
|
||||
|
@ -368,7 +378,7 @@ bool sunrise_sunset_face_loop(movement_event_t event, movement_settings_t *setti
|
|||
}
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
if (_location_count == 1) break;
|
||||
if (_location_count <= 1) break;
|
||||
else if (!state->page) movement_illuminate_led();
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
|
|
|
@ -27,47 +27,162 @@
|
|||
#include "tally_face.h"
|
||||
#include "watch.h"
|
||||
|
||||
#define TALLY_FACE_MAX 9999
|
||||
#define TALLY_FACE_MIN -99
|
||||
|
||||
static bool _init_val;
|
||||
static bool _quick_ticks_running;
|
||||
|
||||
static const int16_t _tally_default[] = {
|
||||
0,
|
||||
|
||||
#ifdef TALLY_FACE_PRESETS_MTG
|
||||
20,
|
||||
40,
|
||||
#endif /* TALLY_FACE_PRESETS_MTG */
|
||||
|
||||
#ifdef TALLY_FACE_PRESETS_YUGIOH
|
||||
4000,
|
||||
8000,
|
||||
#endif /* TALLY_FACE_PRESETS_YUGIOH */
|
||||
|
||||
};
|
||||
|
||||
#define TALLY_FACE_PRESETS_SIZE() (sizeof(_tally_default) / sizeof(int16_t))
|
||||
|
||||
void tally_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(tally_state_t));
|
||||
memset(*context_ptr, 0, sizeof(tally_state_t));
|
||||
tally_state_t *state = (tally_state_t *)*context_ptr;
|
||||
state->tally_default_idx = 0;
|
||||
state->tally_idx = _tally_default[state->tally_default_idx];
|
||||
_init_val = true;
|
||||
}
|
||||
}
|
||||
|
||||
void tally_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
_quick_ticks_running = false;
|
||||
}
|
||||
|
||||
static void start_quick_cyc(void){
|
||||
_quick_ticks_running = true;
|
||||
movement_request_tick_frequency(8);
|
||||
}
|
||||
|
||||
static void stop_quick_cyc(void){
|
||||
_quick_ticks_running = false;
|
||||
movement_request_tick_frequency(1);
|
||||
}
|
||||
|
||||
static void tally_face_increment(tally_state_t *state, bool sound_on) {
|
||||
bool soundOn = !_quick_ticks_running && sound_on;
|
||||
_init_val = false;
|
||||
if (state->tally_idx >= TALLY_FACE_MAX){
|
||||
if (soundOn) watch_buzzer_play_note(BUZZER_NOTE_E7, 30);
|
||||
}
|
||||
else {
|
||||
state->tally_idx++;
|
||||
print_tally(state, sound_on);
|
||||
if (soundOn) watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
|
||||
}
|
||||
}
|
||||
|
||||
static void tally_face_decrement(tally_state_t *state, bool sound_on) {
|
||||
bool soundOn = !_quick_ticks_running && sound_on;
|
||||
_init_val = false;
|
||||
if (state->tally_idx <= TALLY_FACE_MIN){
|
||||
if (soundOn) watch_buzzer_play_note(BUZZER_NOTE_C5SHARP_D5FLAT, 30);
|
||||
}
|
||||
else {
|
||||
state->tally_idx--;
|
||||
print_tally(state, sound_on);
|
||||
if (soundOn) watch_buzzer_play_note(BUZZER_NOTE_C6SHARP_D6FLAT, 30);
|
||||
}
|
||||
}
|
||||
|
||||
static bool tally_face_should_move_back(tally_state_t *state) {
|
||||
if (TALLY_FACE_PRESETS_SIZE() <= 1) { return false; }
|
||||
return state->tally_idx == _tally_default[state->tally_default_idx];
|
||||
}
|
||||
|
||||
bool tally_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
tally_state_t *state = (tally_state_t *)context;
|
||||
static bool using_led = false;
|
||||
|
||||
if (using_led) {
|
||||
if(!watch_get_pin_level(BTN_MODE) && !watch_get_pin_level(BTN_LIGHT) && !watch_get_pin_level(BTN_ALARM))
|
||||
using_led = false;
|
||||
else {
|
||||
if (event.event_type == EVENT_LIGHT_BUTTON_DOWN || event.event_type == EVENT_ALARM_BUTTON_DOWN)
|
||||
movement_illuminate_led();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
// increment tally index
|
||||
state->tally_idx++;
|
||||
if (state->tally_idx > 999999) { //0-999,999
|
||||
//reset tally index and play a reset tune
|
||||
state->tally_idx = 0;
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G6, 30);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_REST, 30);
|
||||
case EVENT_TICK:
|
||||
if (_quick_ticks_running) {
|
||||
bool light_pressed = watch_get_pin_level(BTN_LIGHT);
|
||||
bool alarm_pressed = watch_get_pin_level(BTN_ALARM);
|
||||
if (light_pressed && alarm_pressed) stop_quick_cyc();
|
||||
else if (light_pressed) tally_face_increment(state, settings->bit.button_should_sound);
|
||||
else if (alarm_pressed) tally_face_decrement(state, settings->bit.button_should_sound);
|
||||
else stop_quick_cyc();
|
||||
}
|
||||
print_tally(state);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
tally_face_decrement(state, settings->bit.button_should_sound);
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
state->tally_idx = 0; // reset tally index
|
||||
tally_face_decrement(state, settings->bit.button_should_sound);
|
||||
start_quick_cyc();
|
||||
break;
|
||||
case EVENT_MODE_LONG_PRESS:
|
||||
if (tally_face_should_move_back(state)) {
|
||||
_init_val = true;
|
||||
movement_move_to_face(0);
|
||||
}
|
||||
else {
|
||||
state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index
|
||||
_init_val = true;
|
||||
//play a reset tune
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G6, 30);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_REST, 30);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
|
||||
print_tally(state);
|
||||
if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_G6, 30);
|
||||
if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_REST, 30);
|
||||
if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
|
||||
print_tally(state, settings->bit.button_should_sound);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
tally_face_increment(state, settings->bit.button_should_sound);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
if (watch_get_pin_level(BTN_MODE)) {
|
||||
movement_illuminate_led();
|
||||
using_led = true;
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){
|
||||
state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE();
|
||||
state->tally_idx = _tally_default[state->tally_default_idx];
|
||||
if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
|
||||
if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_REST, 30);
|
||||
if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_G6, 30);
|
||||
print_tally(state, settings->bit.button_should_sound);
|
||||
}
|
||||
else{
|
||||
tally_face_increment(state, settings->bit.button_should_sound);
|
||||
start_quick_cyc();
|
||||
}
|
||||
break;
|
||||
case EVENT_ACTIVATE:
|
||||
print_tally(state);
|
||||
print_tally(state, settings->bit.button_should_sound);
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
// ignore timeout
|
||||
|
@ -81,9 +196,16 @@ bool tally_face_loop(movement_event_t event, movement_settings_t *settings, void
|
|||
}
|
||||
|
||||
// print tally index at the center of display.
|
||||
void print_tally(tally_state_t *state) {
|
||||
void print_tally(tally_state_t *state, bool sound_on) {
|
||||
char buf[14];
|
||||
sprintf(buf, "TA %06d", (int)(state->tally_idx)); // center of LCD display
|
||||
if (sound_on)
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
else
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
if (state->tally_idx >= 0)
|
||||
sprintf(buf, "TA %4d ", (int)(state->tally_idx)); // center of LCD display
|
||||
else
|
||||
sprintf(buf, "TA %-3d", (int)(state->tally_idx)); // center of LCD display
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
|
|
|
@ -29,25 +29,41 @@
|
|||
* TALLY face
|
||||
*
|
||||
* Tally face is designed to act as a tally counter.
|
||||
* Based on the counter_face watch face by Shogo Okamoto.
|
||||
*
|
||||
* To advance the counter, press the ALARM button.
|
||||
* To reset, long press the ALARM button.
|
||||
* Alarm
|
||||
* Press: Decrement
|
||||
* Hold : Fast Decrement
|
||||
*
|
||||
* Light
|
||||
* Press: Increment
|
||||
* Hold : On initial value: Cycles through other initial values.
|
||||
* Else: Fast Increment
|
||||
*
|
||||
* Mode
|
||||
* Press: Next face
|
||||
* Hold : On initial value: Go to first face.
|
||||
* Else: Resets counter
|
||||
*
|
||||
* Incrementing or Decrementing the tally will beep if Beeping is set in the global Preferences
|
||||
*/
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
typedef struct {
|
||||
uint32_t tally_idx;
|
||||
int16_t tally_idx;
|
||||
uint8_t tally_default_idx;
|
||||
} tally_state_t;
|
||||
|
||||
//#define TALLY_FACE_PRESETS_MTG
|
||||
//#define TALLY_FACE_PRESETS_YUGIOH
|
||||
|
||||
|
||||
void tally_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void tally_face_activate(movement_settings_t *settings, void *context);
|
||||
bool tally_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void tally_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
void print_tally(tally_state_t *state);
|
||||
void print_tally(tally_state_t *state, bool sound_on);
|
||||
|
||||
#define tally_face ((const watch_face_t){ \
|
||||
tally_face_setup, \
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include "time_left_face.h"
|
||||
#include "watch.h"
|
||||
#include "watch_private_display.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
const char _state_titles[][3] = {{'D', 'L', ' '}, {'D', 'L', ' '}, {'D', 'A', ' '}, {'D', 'A', ' '}, {'Y', 'R', 'b'}, {'M', 'O', 'b'}, {'D', 'A', 'b'},
|
||||
{'Y', 'R', 'd'}, {'M', 'O', 'd'}, {'D', 'A', 'd'}};
|
||||
|
@ -158,8 +159,6 @@ static void _draw(time_left_state_t *state, uint8_t subsecond) {
|
|||
/// @brief handle short or long pressing the alarm button
|
||||
static void _handle_alarm_button(time_left_state_t *state) {
|
||||
|
||||
const uint8_t days_in_month[12] = {31, 28, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31};
|
||||
uint32_t tmp_day;
|
||||
switch (state->current_page) {
|
||||
case TIME_LEFT_FACE_SETTINGS_STATE: // birth year
|
||||
state->birth_date.bit.year++;
|
||||
|
@ -169,14 +168,7 @@ static void _handle_alarm_button(time_left_state_t *state) {
|
|||
state->birth_date.bit.month = (state->birth_date.bit.month % 12) + 1;
|
||||
break;
|
||||
case TIME_LEFT_FACE_SETTINGS_STATE + 2: // birth day
|
||||
tmp_day = state->birth_date.bit.day; // use a temporary variable to avoid messing up the months
|
||||
tmp_day++;
|
||||
// handle February 29th on a leap year
|
||||
if (((tmp_day > days_in_month[state->birth_date.bit.month - 1]) && (state->birth_date.bit.month != 2 || (state->birth_date.bit.year % 4) != 0))
|
||||
|| (state->birth_date.bit.month == 2 && (state->birth_date.bit.year % 4) == 0 && tmp_day > 29)) {
|
||||
tmp_day = 1;
|
||||
}
|
||||
state->birth_date.bit.day = tmp_day;
|
||||
state->birth_date.bit.day++;
|
||||
break;
|
||||
case TIME_LEFT_FACE_SETTINGS_STATE + 3: // target year
|
||||
state->target_date.bit.year++;
|
||||
|
@ -186,16 +178,13 @@ static void _handle_alarm_button(time_left_state_t *state) {
|
|||
state->target_date.bit.month = (state->target_date.bit.month % 12) + 1;
|
||||
break;
|
||||
case TIME_LEFT_FACE_SETTINGS_STATE + 5: // target day
|
||||
tmp_day = state->target_date.bit.day;
|
||||
tmp_day++;
|
||||
// handle February 29th on a leap year
|
||||
if (((tmp_day > days_in_month[state->target_date.bit.month - 1]) && (state->target_date.bit.month != 2 || (state->target_date.bit.year % 4) != 0))
|
||||
|| (state->target_date.bit.month == 2 && (state->target_date.bit.year % 4) == 0 && tmp_day > 29)) {
|
||||
tmp_day = 1;
|
||||
}
|
||||
state->target_date.bit.day = tmp_day;
|
||||
state->target_date.bit.day++;
|
||||
break;
|
||||
}
|
||||
if (state->birth_date.bit.day > days_in_month(state->birth_date.bit.month, state->birth_date.bit.year))
|
||||
state->birth_date.bit.day = 1;
|
||||
if (state->target_date.bit.day > days_in_month(state->target_date.bit.month, state->birth_date.bit.year))
|
||||
state->target_date.bit.day = 1;
|
||||
}
|
||||
|
||||
static void _initiate_setting(time_left_state_t *state) {
|
||||
|
|
|
@ -48,15 +48,19 @@
|
|||
* o SHA512
|
||||
*
|
||||
* Instructions:
|
||||
* o Find your secret key(s) and convert them to the required format.
|
||||
* o Use https://cryptii.com/pipes/base32-to-hex to convert base32 to hex
|
||||
* o Use https://github.com/susam/mintotp to generate test codes for verification
|
||||
* o Edit global variables in "totp_face.c" to configure your stored keys:
|
||||
* o "keys", "key_sizes", "timesteps", and "algorithms" set the
|
||||
* cryptographic parameters for each secret key.
|
||||
* o "labels" sets the two-letter label for each key
|
||||
* (This replaces the day-of-week indicator)
|
||||
* o Once finished, remove the two provided examples.
|
||||
* o Find your secret key(s).
|
||||
* o Use https://github.com/susam/mintotp to generate test codes for
|
||||
* verification
|
||||
* o Edit global `credentials` variable in "totp_face.c" to configure your
|
||||
* TOTP credentials. The file includes two examples that you can use as a
|
||||
* reference. Credentials are added with the `CREDENTIAL` macro in the form
|
||||
* `CREDENTIAL(label, key, algorithm, timestep)` where:
|
||||
* o `label` is a 2 character label that is displayed in the weekday digits
|
||||
* to identify the TOTP credential.
|
||||
* o `key` is a string with the base32 encoded secret.
|
||||
* o `algorithm` is one of the supported hashing algorithms listed above.
|
||||
* o `timestep` is how often the TOTP refreshes in seconds. This is usually
|
||||
* 30 seconds.
|
||||
*
|
||||
* If you have more than one secret key, press ALARM to cycle through them.
|
||||
* Press LIGHT to cycle in the other direction or keep it pressed longer to
|
||||
|
|
|
@ -38,12 +38,15 @@ void _wake_face_update_display(movement_settings_t *settings, wake_face_state_t
|
|||
uint8_t hour = state->hour;
|
||||
|
||||
watch_clear_display();
|
||||
if ( settings->bit.clock_mode_24h )
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
else {
|
||||
bool set_leading_zero = false;
|
||||
if ( !settings->bit.clock_mode_24h ) {
|
||||
if ( hour >= 12 )
|
||||
watch_set_indicator(WATCH_INDICATOR_PM);
|
||||
hour = hour % 12 ? hour % 12 : 12;
|
||||
} else if ( !settings->bit.clock_24h_leading_zero ) {
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
} else if ( hour < 10 ) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
|
||||
if ( state->mode )
|
||||
|
@ -54,6 +57,8 @@ void _wake_face_update_display(movement_settings_t *settings, wake_face_state_t
|
|||
|
||||
watch_set_colon();
|
||||
watch_display_string(lcdbuf, 0);
|
||||
if ( set_leading_zero )
|
||||
watch_display_string("0", 4);
|
||||
}
|
||||
|
||||
//
|
||||
|
|
234
movement/watch_faces/complication/wareki_face.c
Normal file
234
movement/watch_faces/complication/wareki_face.c
Normal file
|
@ -0,0 +1,234 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "wareki_face.h"
|
||||
#include "filesystem.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
|
||||
//Long press status flag
|
||||
static bool _alarm_button_press;
|
||||
static bool _light_button_press;
|
||||
|
||||
|
||||
void wareki_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
|
||||
(void) watch_face_index;
|
||||
|
||||
//printf("wareki_setup() \n");
|
||||
(void) settings;
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(wareki_state_t));
|
||||
memset(*context_ptr, 0, sizeof(wareki_state_t));
|
||||
// Do any one-time tasks in here; the inside of this conditional happens only at boot.
|
||||
|
||||
//debug code
|
||||
// watch_date_time datetime = watch_rtc_get_date_time();
|
||||
// datetime.unit.year = 2022 - WATCH_RTC_REFERENCE_YEAR;
|
||||
// datetime.unit.month = 12;
|
||||
// datetime.unit.day = 31;
|
||||
// datetime.unit.hour = 23;
|
||||
// datetime.unit.minute= 59;
|
||||
// datetime.unit.second= 30;
|
||||
// watch_rtc_set_date_time(datetime);
|
||||
// settings->bit.clock_mode_24h = true; //24時間表記
|
||||
// settings->bit.to_interval = 1;//0=60sec 1=2m 2=5m 3=30m
|
||||
// watch_store_backup_data(settings->reg, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// splash view
|
||||
static void draw_wareki_splash(wareki_state_t *state) {
|
||||
(void) state;
|
||||
char buf[11];
|
||||
|
||||
watch_clear_colon();
|
||||
|
||||
sprintf(buf, "%s","wa ------");
|
||||
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
|
||||
//draw year and Japanese wareki
|
||||
static void draw_year_and_wareki(wareki_state_t *state) {
|
||||
char buf[27];
|
||||
|
||||
if(state->disp_year < REIWA_GANNEN){
|
||||
//Heisei
|
||||
sprintf(buf, " h%2d%4d ", (int)state->disp_year - HEISEI_GANNEN + 1, (int)state->disp_year);
|
||||
}
|
||||
else{
|
||||
//Reiwa
|
||||
sprintf(buf, " r%2d%4d ", (int)state->disp_year - REIWA_GANNEN + 1 , (int)state->disp_year);
|
||||
}
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
|
||||
void wareki_activate(movement_settings_t *settings, void *context) {
|
||||
|
||||
//printf("wareki_activate() \n");
|
||||
|
||||
(void) settings;
|
||||
wareki_state_t *state = (wareki_state_t *)context;
|
||||
|
||||
if (watch_tick_animation_is_running()) watch_stop_tick_animation();
|
||||
|
||||
state->active = false;
|
||||
|
||||
_alarm_button_press = false;
|
||||
_light_button_press = false;
|
||||
|
||||
state->real_year = watch_rtc_get_date_time().unit.year + WATCH_RTC_REFERENCE_YEAR;
|
||||
state->start_year = state->real_year;
|
||||
state->disp_year = state->real_year;
|
||||
|
||||
movement_request_tick_frequency(1);
|
||||
}
|
||||
|
||||
|
||||
void addYear(wareki_state_t* state,int count){
|
||||
|
||||
state->disp_year = state->disp_year + count;
|
||||
|
||||
if(state->disp_year > REIWA_LIMIT ){
|
||||
state->disp_year = REIWA_LIMIT;
|
||||
}
|
||||
else{
|
||||
//watch_buzzer_play_note(BUZZER_NOTE_C8, 30);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void subYear(wareki_state_t* state,int count){
|
||||
|
||||
state->disp_year = state->disp_year - count;
|
||||
|
||||
if(state->disp_year < 1989 ){
|
||||
state->disp_year = 1989;
|
||||
}
|
||||
else{
|
||||
//watch_buzzer_play_note(BUZZER_NOTE_C7, 30);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool wareki_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
wareki_state_t *state = (wareki_state_t *)context;
|
||||
|
||||
state->real_year = watch_rtc_get_date_time().unit.year + WATCH_RTC_REFERENCE_YEAR;
|
||||
|
||||
if( state->real_year != state->start_year ){
|
||||
state->start_year = state->real_year;
|
||||
state->disp_year = state->real_year;
|
||||
}
|
||||
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
draw_wareki_splash(state);
|
||||
break;
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
movement_move_to_next_face();
|
||||
break;
|
||||
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
case EVENT_TICK:
|
||||
|
||||
//printf("tick %d\n",state->disp_year );
|
||||
|
||||
if (_alarm_button_press && watch_get_pin_level(BTN_ALARM)){
|
||||
//printf("ALARM ON\n");
|
||||
}
|
||||
else{
|
||||
//printf("ALARM OFF\n");
|
||||
_alarm_button_press = false;
|
||||
}
|
||||
|
||||
if (_light_button_press && watch_get_pin_level(BTN_LIGHT)){
|
||||
//printf("LIGHT ON\n");
|
||||
}
|
||||
else{
|
||||
//printf("LIGHT OFF\n");
|
||||
_light_button_press = false;
|
||||
}
|
||||
|
||||
if (_alarm_button_press) {
|
||||
addYear(state,1);
|
||||
}
|
||||
if (_light_button_press) {
|
||||
subYear(state,1);
|
||||
}
|
||||
|
||||
draw_year_and_wareki(state);
|
||||
|
||||
break;
|
||||
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
//printf("LIGHT DOWN\n");
|
||||
subYear(state,1);
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
//printf("LIGHTPRESS \n");
|
||||
_light_button_press = true;
|
||||
movement_request_tick_frequency(8);
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_UP:
|
||||
//printf("LIGHTPRESS UP\n");
|
||||
_light_button_press = false;
|
||||
movement_request_tick_frequency(4);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
//printf("LIGHT UP\n");
|
||||
_light_button_press = false;
|
||||
movement_request_tick_frequency(4);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
//printf("ALARM DOWN\n");
|
||||
addYear(state,1);
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
//printf("LONGPRESS \n");
|
||||
_alarm_button_press = true;
|
||||
movement_request_tick_frequency(8);
|
||||
break;
|
||||
case EVENT_ALARM_LONG_UP:
|
||||
//printf("LONGPRESS UP\n");
|
||||
_alarm_button_press = false;
|
||||
movement_request_tick_frequency(4);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
//printf("ALARM UP\n");
|
||||
movement_request_tick_frequency(4);
|
||||
break;
|
||||
|
||||
case EVENT_TIMEOUT:
|
||||
//printf("time out ! \n");
|
||||
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;
|
||||
}
|
||||
|
||||
void wareki_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
34
movement/watch_faces/complication/wareki_face.h
Normal file
34
movement/watch_faces/complication/wareki_face.h
Normal file
|
@ -0,0 +1,34 @@
|
|||
#ifndef WAREKI_FACE_H_
|
||||
#define WAREKI_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
#define REIWA_LIMIT 2018 + 31
|
||||
#define REIWA_GANNEN 2019
|
||||
#define HEISEI_GANNEN 1989
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
uint32_t disp_year; //Current displayed year
|
||||
uint32_t start_year; //Year when this screen was launched
|
||||
uint32_t real_year; //The actual current year
|
||||
} wareki_state_t;
|
||||
|
||||
|
||||
void wareki_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void wareki_activate(movement_settings_t *settings, void *context);
|
||||
bool wareki_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void wareki_resign(movement_settings_t *settings, void *context);
|
||||
void addYear(wareki_state_t* state,int count);
|
||||
void subYear(wareki_state_t* state,int count);
|
||||
|
||||
#define wareki_face ((const watch_face_t){ \
|
||||
wareki_setup, \
|
||||
wareki_activate, \
|
||||
wareki_loop, \
|
||||
wareki_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // WAREKI_FACE_H_
|
||||
|
602
movement/watch_faces/complication/wordle_face.c
Normal file
602
movement/watch_faces/complication/wordle_face.c
Normal file
|
@ -0,0 +1,602 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 <David Volovskiy>
|
||||
*
|
||||
* 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 "wordle_face.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
static uint32_t get_random(uint32_t max) {
|
||||
#if __EMSCRIPTEN__
|
||||
return rand() % max;
|
||||
#else
|
||||
return arc4random_uniform(max);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t get_first_pos(WordleLetterResult *word_elements_result) {
|
||||
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
|
||||
if (word_elements_result[i] != WORDLE_LETTER_CORRECT)
|
||||
return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint8_t get_next_pos(uint8_t curr_pos, WordleLetterResult *word_elements_result) {
|
||||
for (size_t pos = curr_pos; pos < WORDLE_LENGTH;) {
|
||||
if (word_elements_result[++pos] != WORDLE_LETTER_CORRECT)
|
||||
return pos;
|
||||
}
|
||||
return WORDLE_LENGTH;
|
||||
}
|
||||
|
||||
static uint8_t get_prev_pos(uint8_t curr_pos, WordleLetterResult *word_elements_result) {
|
||||
if (curr_pos == 0) return 0;
|
||||
for (int8_t pos = curr_pos; pos >= 0;) {
|
||||
if (word_elements_result[--pos] != WORDLE_LETTER_CORRECT)
|
||||
return pos;
|
||||
}
|
||||
return curr_pos;
|
||||
}
|
||||
|
||||
static void get_next_letter(const uint8_t curr_pos, uint8_t *word_elements, const bool *known_wrong_letters, const bool skip_wrong_letter) {
|
||||
do {
|
||||
if (word_elements[curr_pos] >= WORDLE_NUM_VALID_LETTERS) word_elements[curr_pos] = 0;
|
||||
else word_elements[curr_pos] = (word_elements[curr_pos] + 1) % WORDLE_NUM_VALID_LETTERS;
|
||||
} while (skip_wrong_letter && known_wrong_letters[word_elements[curr_pos]]);
|
||||
}
|
||||
|
||||
static void get_prev_letter(const uint8_t curr_pos, uint8_t *word_elements, const bool *known_wrong_letters, const bool skip_wrong_letter) {
|
||||
do {
|
||||
if (word_elements[curr_pos] >= WORDLE_NUM_VALID_LETTERS) word_elements[curr_pos] = WORDLE_NUM_VALID_LETTERS - 1;
|
||||
else word_elements[curr_pos] = (word_elements[curr_pos] + WORDLE_NUM_VALID_LETTERS - 1) % WORDLE_NUM_VALID_LETTERS;
|
||||
} while (skip_wrong_letter && known_wrong_letters[word_elements[curr_pos]]);
|
||||
}
|
||||
|
||||
static void display_letter(wordle_state_t *state, bool display_dash) {
|
||||
char buf[1 + 1];
|
||||
if (state->word_elements[state->position] >= WORDLE_NUM_VALID_LETTERS) {
|
||||
if (display_dash)
|
||||
watch_display_string("-", state->position + 5);
|
||||
else
|
||||
watch_display_string(" ", state->position + 5);
|
||||
return;
|
||||
}
|
||||
sprintf(buf, "%c", _valid_letters[state->word_elements[state->position]]);
|
||||
watch_display_string(buf, state->position + 5);
|
||||
}
|
||||
|
||||
static void display_all_letters(wordle_state_t *state) {
|
||||
uint8_t prev_pos = state->position;
|
||||
watch_display_string(" ", 4);
|
||||
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
|
||||
state->position = i;
|
||||
display_letter(state, false);
|
||||
}
|
||||
state->position = prev_pos;
|
||||
}
|
||||
|
||||
#if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES
|
||||
static void display_not_in_dict(wordle_state_t *state) {
|
||||
state->curr_screen = SCREEN_NO_DICT;
|
||||
watch_display_string("nodict", 4);
|
||||
}
|
||||
|
||||
static void display_already_guessed(wordle_state_t *state) {
|
||||
state->curr_screen = SCREEN_ALREADY_GUESSED;
|
||||
watch_display_string("GUESSD", 4);
|
||||
}
|
||||
|
||||
static uint32_t check_word_in_dict(uint8_t *word_elements) {
|
||||
bool is_exact_match;
|
||||
for (uint16_t i = 0; i < WORDLE_NUM_WORDS; i++) {
|
||||
is_exact_match = true;
|
||||
for (size_t j = 0; j < WORDLE_LENGTH; j++) {
|
||||
if (_valid_letters[word_elements[j]] != _valid_words[i][j]) {
|
||||
is_exact_match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_exact_match) return i;
|
||||
}
|
||||
for (uint16_t i = 0; i < WORDLE_NUM_POSSIBLE_WORDS; i++) {
|
||||
is_exact_match = true;
|
||||
for (size_t j = 0; j < WORDLE_LENGTH; j++) {
|
||||
if (_valid_letters[word_elements[j]] != _possible_words[i][j]) {
|
||||
is_exact_match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_exact_match) return WORDLE_NUM_WORDS + i;
|
||||
}
|
||||
return WORDLE_NUM_WORDS + WORDLE_NUM_POSSIBLE_WORDS;
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool check_word(wordle_state_t *state) {
|
||||
// Exact
|
||||
bool is_exact_match = true;
|
||||
bool answer_already_accounted[WORDLE_LENGTH] = { false };
|
||||
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
|
||||
if (_valid_letters[state->word_elements[i]] == _valid_words[state->curr_answer][i]) {
|
||||
state->word_elements_result[i] = WORDLE_LETTER_CORRECT;
|
||||
answer_already_accounted[i] = true;
|
||||
}
|
||||
else {
|
||||
state->word_elements_result[i] = WORDLE_LETTER_WRONG;
|
||||
is_exact_match = false;
|
||||
}
|
||||
}
|
||||
if (is_exact_match) return true;
|
||||
// Wrong Location
|
||||
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
|
||||
if (state->word_elements_result[i] != WORDLE_LETTER_WRONG) continue;
|
||||
for (size_t j = 0; j < WORDLE_LENGTH; j++) {
|
||||
if (answer_already_accounted[j]) continue;
|
||||
if (_valid_letters[state->word_elements[i]] == _valid_words[state->curr_answer][j]) {
|
||||
state->word_elements_result[i] = WORDLE_LETTER_WRONG_LOC;
|
||||
answer_already_accounted[j] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void show_skip_wrong_letter_indicator(bool skipping, WordleScreen curr_screen) {
|
||||
if (curr_screen >= SCREEN_PLAYING) return;
|
||||
if (skipping)
|
||||
watch_display_string("H", 3);
|
||||
else
|
||||
watch_display_string(" ", 3);
|
||||
}
|
||||
|
||||
static void update_known_wrong_letters(wordle_state_t *state) {
|
||||
bool wrong_loc[WORDLE_NUM_VALID_LETTERS] = {false};
|
||||
// To ignore letters that appear, but are in the wrong location, as letters that are guessed
|
||||
// more often than they appear in the word will display as WORDLE_LETTER_WRONG
|
||||
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
|
||||
if (state->word_elements_result[i] == WORDLE_LETTER_WRONG_LOC) {
|
||||
for (size_t j = 0; j < WORDLE_NUM_VALID_LETTERS; j++) {
|
||||
if (state->word_elements[i] == j)
|
||||
wrong_loc[j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
|
||||
if (state->word_elements_result[i] == WORDLE_LETTER_WRONG) {
|
||||
for (size_t j = 0; j < WORDLE_NUM_VALID_LETTERS; j++) {
|
||||
if (state->word_elements[i] == j && !wrong_loc[j])
|
||||
state->known_wrong_letters[j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void display_attempt(uint8_t attempt) {
|
||||
char buf[3];
|
||||
sprintf(buf, "%d", attempt+1);
|
||||
watch_display_string(buf, 3);
|
||||
}
|
||||
|
||||
static void display_playing(wordle_state_t *state) {
|
||||
state->curr_screen = SCREEN_PLAYING;
|
||||
display_attempt(state->attempt);
|
||||
display_all_letters(state);
|
||||
}
|
||||
|
||||
static void reset_all_elements(wordle_state_t *state) {
|
||||
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
|
||||
state->word_elements[i] = WORDLE_NUM_VALID_LETTERS;
|
||||
state->word_elements_result[i] = WORDLE_LETTER_WRONG;
|
||||
}
|
||||
for (size_t i = 0; i < WORDLE_NUM_VALID_LETTERS; i++){
|
||||
state->known_wrong_letters[i] = false;
|
||||
}
|
||||
#if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES
|
||||
for (size_t i = 0; i < WORDLE_MAX_ATTEMPTS; i++) {
|
||||
state->guessed_words[i] = WORDLE_NUM_WORDS + WORDLE_NUM_POSSIBLE_WORDS;
|
||||
}
|
||||
#endif
|
||||
state->using_random_guess = false;
|
||||
state->attempt = 0;
|
||||
}
|
||||
|
||||
static void reset_incorrect_elements(wordle_state_t *state) {
|
||||
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
|
||||
if (state->word_elements_result[i] != WORDLE_LETTER_CORRECT)
|
||||
state->word_elements[i] = WORDLE_NUM_VALID_LETTERS;
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_board(wordle_state_t *state) {
|
||||
reset_all_elements(state);
|
||||
state->curr_answer = get_random(WORDLE_NUM_WORDS);
|
||||
watch_clear_colon();
|
||||
state->position = get_first_pos(state->word_elements_result);
|
||||
display_playing(state);
|
||||
watch_display_string(" -", 4);
|
||||
#if __EMSCRIPTEN__
|
||||
printf("ANSWER: %s\r\n", _valid_words[state->curr_answer]);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void display_title(wordle_state_t *state) {
|
||||
state->curr_screen = SCREEN_TITLE;
|
||||
watch_display_string("WO WordLE", 0);
|
||||
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
|
||||
}
|
||||
|
||||
#if WORDLE_USE_DAILY_STREAK != 2
|
||||
static void display_continue_result(bool continuing) {
|
||||
watch_display_string(continuing ? "y" : "n", 9);
|
||||
}
|
||||
|
||||
static void display_continue(wordle_state_t *state) {
|
||||
state->curr_screen = SCREEN_CONTINUE;
|
||||
watch_display_string("Cont ", 4);
|
||||
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
|
||||
display_continue_result(state->continuing);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void display_streak(wordle_state_t *state) {
|
||||
char buf[12];
|
||||
state->curr_screen = SCREEN_STREAK;
|
||||
#if WORDLE_USE_DAILY_STREAK == 2
|
||||
if (state->streak > 99)
|
||||
sprintf(buf, "WO St--dy");
|
||||
else
|
||||
sprintf(buf, "WO St%2ddy", state->streak);
|
||||
#else
|
||||
sprintf(buf, "WO St%4d", state->streak);
|
||||
#endif
|
||||
watch_display_string(buf, 0);
|
||||
watch_set_colon();
|
||||
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
|
||||
}
|
||||
|
||||
#if WORDLE_USE_DAILY_STREAK == 2
|
||||
static void display_wait(wordle_state_t *state) {
|
||||
state->curr_screen = SCREEN_WAIT;
|
||||
if (state->streak < 40) {
|
||||
char buf[13];
|
||||
sprintf(buf,"WO%2d WaIt ", state->streak);
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
else { // Streak too long to display in top-right
|
||||
watch_display_string("WO WaIt ", 0);
|
||||
}
|
||||
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
|
||||
}
|
||||
#endif
|
||||
|
||||
static uint32_t get_day_unix_time(void) {
|
||||
watch_date_time now = watch_rtc_get_date_time();
|
||||
#if WORDLE_USE_DAILY_STREAK == 2
|
||||
now.unit.hour = now.unit.minute = now.unit.second = 0;
|
||||
#endif
|
||||
return watch_utility_date_time_to_unix_time(now, 0);
|
||||
}
|
||||
|
||||
static void display_lose(wordle_state_t *state, uint8_t subsecond) {
|
||||
char buf[WORDLE_LENGTH + 6];
|
||||
sprintf(buf," L %s", subsecond % 2 ? _valid_words[state->curr_answer] : " ");
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void display_win(wordle_state_t *state, uint8_t subsecond) {
|
||||
(void) state;
|
||||
char buf[13];
|
||||
sprintf(buf," W %s ", subsecond % 2 ? "NICE" : "JOb ");
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static bool is_playing(const wordle_state_t *state) {
|
||||
if (state->attempt > 0) return true;
|
||||
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
|
||||
if (state->word_elements[i] != WORDLE_NUM_VALID_LETTERS)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void display_result(wordle_state_t *state, uint8_t subsecond) {
|
||||
char buf[WORDLE_LENGTH + 1];
|
||||
for (size_t i = 0; i < WORDLE_LENGTH; i++)
|
||||
{
|
||||
switch (state->word_elements_result[i])
|
||||
{
|
||||
case WORDLE_LETTER_WRONG:
|
||||
buf[i] = '-';
|
||||
break;
|
||||
case WORDLE_LETTER_CORRECT:
|
||||
buf[i] = _valid_letters[state->word_elements[i]];
|
||||
break;
|
||||
case WORDLE_LETTER_WRONG_LOC:
|
||||
if (subsecond % 2)
|
||||
buf[i] = ' ';
|
||||
else
|
||||
buf[i] = _valid_letters[state->word_elements[i]];
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
watch_display_string(buf, 5);
|
||||
}
|
||||
|
||||
static bool act_on_btn(wordle_state_t *state, const uint8_t pin) {
|
||||
switch (state->curr_screen)
|
||||
{
|
||||
case SCREEN_RESULT:
|
||||
reset_incorrect_elements(state);
|
||||
state->position = get_first_pos(state->word_elements_result);
|
||||
display_playing(state);
|
||||
return true;
|
||||
case SCREEN_TITLE:
|
||||
#if WORDLE_USE_DAILY_STREAK == 2
|
||||
if (state->day_last_game_started == get_day_unix_time()) {
|
||||
display_wait(state);
|
||||
}
|
||||
else if (is_playing(state))
|
||||
display_playing(state);
|
||||
else
|
||||
display_streak(state);
|
||||
#else
|
||||
if (is_playing(state)) {
|
||||
state->continuing = true;
|
||||
display_continue(state);
|
||||
}
|
||||
else
|
||||
display_streak(state);
|
||||
#endif
|
||||
return true;
|
||||
case SCREEN_STREAK:
|
||||
state->day_last_game_started = get_day_unix_time();
|
||||
reset_board(state);
|
||||
return true;
|
||||
case SCREEN_WIN:
|
||||
case SCREEN_LOSE:
|
||||
display_title(state);
|
||||
return true;
|
||||
case SCREEN_NO_DICT:
|
||||
case SCREEN_ALREADY_GUESSED:
|
||||
state->position = get_first_pos(state->word_elements_result);
|
||||
display_playing(state);
|
||||
return true;
|
||||
#if WORDLE_USE_DAILY_STREAK == 2
|
||||
case SCREEN_WAIT:
|
||||
(void) pin;
|
||||
display_title(state);
|
||||
return true;
|
||||
#else
|
||||
case SCREEN_CONTINUE:
|
||||
switch (pin)
|
||||
{
|
||||
case BTN_ALARM:
|
||||
if (state->continuing)
|
||||
display_playing(state);
|
||||
else {
|
||||
reset_board(state);
|
||||
state->streak = 0;
|
||||
display_streak(state);
|
||||
}
|
||||
break;
|
||||
case BTN_LIGHT:
|
||||
state->continuing = !state->continuing;
|
||||
display_continue_result(state->continuing);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void get_result(wordle_state_t *state) {
|
||||
#if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES
|
||||
// Check if it's in the dict
|
||||
uint16_t in_dict = check_word_in_dict(state->word_elements);
|
||||
if (in_dict == WORDLE_NUM_WORDS + WORDLE_NUM_POSSIBLE_WORDS) {
|
||||
display_not_in_dict(state);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already guessed
|
||||
for (size_t i = 0; i < WORDLE_MAX_ATTEMPTS; i++) {
|
||||
if(in_dict == state->guessed_words[i]) {
|
||||
display_already_guessed(state);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
state->guessed_words[state->attempt] = in_dict;
|
||||
#endif
|
||||
bool exact_match = check_word(state);
|
||||
if (exact_match) {
|
||||
reset_all_elements(state);
|
||||
state->curr_screen = SCREEN_WIN;
|
||||
if (state->streak < 0x7F)
|
||||
state->streak++;
|
||||
#if WORDLE_USE_DAILY_STREAK == 2
|
||||
state->day_last_game_started = get_day_unix_time(); // On the edge-case where we solve the puzzle at midnight
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
if (++state->attempt >= WORDLE_MAX_ATTEMPTS) {
|
||||
reset_all_elements(state);
|
||||
state->curr_screen = SCREEN_LOSE;
|
||||
state->streak = 0;
|
||||
return;
|
||||
}
|
||||
update_known_wrong_letters(state);
|
||||
state->curr_screen = SCREEN_RESULT;
|
||||
return;
|
||||
}
|
||||
|
||||
#if (WORDLE_USE_RANDOM_GUESS != 0)
|
||||
static void insert_random_guess(wordle_state_t *state) {
|
||||
uint16_t random_guess;
|
||||
do { // Don't allow the guess to be the same as the answer
|
||||
random_guess = get_random(_num_random_guess_words);
|
||||
} while (random_guess == state->curr_answer);
|
||||
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
|
||||
for (size_t j = 0; j < WORDLE_NUM_VALID_LETTERS; j++)
|
||||
{
|
||||
if (_valid_words[random_guess][i] == _valid_letters[j])
|
||||
state->word_elements[i] = j;
|
||||
}
|
||||
}
|
||||
state->position = WORDLE_LENGTH - 1;
|
||||
display_all_letters(state);
|
||||
state->using_random_guess = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void wordle_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(wordle_state_t));
|
||||
memset(*context_ptr, 0, sizeof(wordle_state_t));
|
||||
wordle_state_t *state = (wordle_state_t *)*context_ptr;
|
||||
state->curr_screen = SCREEN_TITLE;
|
||||
state->skip_wrong_letter = false;
|
||||
reset_all_elements(state);
|
||||
}
|
||||
// Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.
|
||||
}
|
||||
|
||||
void wordle_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
wordle_state_t *state = (wordle_state_t *)context;
|
||||
#if WORDLE_USE_DAILY_STREAK != 0
|
||||
uint32_t now = get_day_unix_time();
|
||||
uint32_t one_day = 60 *60 * 24;
|
||||
if ((WORDLE_USE_DAILY_STREAK == 2 && now >= (state->day_last_game_started + (2*one_day)))
|
||||
|| (now >= (state->day_last_game_started + one_day) && is_playing(state))) {
|
||||
state->streak = 0;
|
||||
reset_board(state);
|
||||
}
|
||||
#endif
|
||||
state->using_random_guess = false;
|
||||
if (is_playing(state) && state->curr_screen >= SCREEN_RESULT) {
|
||||
reset_incorrect_elements(state);
|
||||
state->position = get_first_pos(state->word_elements_result);
|
||||
}
|
||||
movement_request_tick_frequency(2);
|
||||
display_title(state);
|
||||
}
|
||||
|
||||
bool wordle_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
wordle_state_t *state = (wordle_state_t *)context;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_TICK:
|
||||
switch (state->curr_screen)
|
||||
{
|
||||
case SCREEN_PLAYING:
|
||||
if (event.subsecond % 2) {
|
||||
display_letter(state, true);
|
||||
} else {
|
||||
watch_display_string(" ", state->position + 5);
|
||||
}
|
||||
break;
|
||||
case SCREEN_RESULT:
|
||||
display_result(state, event.subsecond);
|
||||
break;
|
||||
case SCREEN_LOSE:
|
||||
display_lose(state, event.subsecond);
|
||||
break;
|
||||
case SCREEN_WIN:
|
||||
display_win(state, event.subsecond);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
if (act_on_btn(state, BTN_LIGHT)) break;
|
||||
get_next_letter(state->position, state->word_elements, state->known_wrong_letters, state->skip_wrong_letter);
|
||||
display_letter(state, true);
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
if (state->curr_screen < SCREEN_PLAYING) {
|
||||
state->skip_wrong_letter = !state->skip_wrong_letter;
|
||||
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
|
||||
break;
|
||||
}
|
||||
if (state->curr_screen != SCREEN_PLAYING) break;
|
||||
get_prev_letter(state->position, state->word_elements, state->known_wrong_letters, state->skip_wrong_letter);
|
||||
display_letter(state, true);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
if (act_on_btn(state, BTN_ALARM)) break;
|
||||
display_letter(state, true);
|
||||
if (state->word_elements[state->position] == WORDLE_NUM_VALID_LETTERS) break;
|
||||
#if (WORDLE_USE_RANDOM_GUESS != 0)
|
||||
if (watch_get_pin_level(BTN_LIGHT) &&
|
||||
(state->using_random_guess || (state->attempt == 0 && state->position == 0))) {
|
||||
insert_random_guess(state);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
state->position = get_next_pos(state->position, state->word_elements_result);
|
||||
if (state->position >= WORDLE_LENGTH) {
|
||||
get_result(state);
|
||||
state->using_random_guess = false;
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (state->curr_screen != SCREEN_PLAYING) break;
|
||||
display_letter(state, true);
|
||||
state->position = get_prev_pos(state->position, state->word_elements_result);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
case EVENT_ACTIVATE:
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
if (state->curr_screen >= SCREEN_RESULT) {
|
||||
reset_incorrect_elements(state);
|
||||
state->position = get_first_pos(state->word_elements_result);
|
||||
display_title(state);
|
||||
}
|
||||
break;
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
if (state->curr_screen != SCREEN_TITLE)
|
||||
display_title(state);
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void wordle_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
149
movement/watch_faces/complication/wordle_face.h
Normal file
149
movement/watch_faces/complication/wordle_face.h
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 <David Volovskiy>
|
||||
*
|
||||
* 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 WORDLE_FACE_H_
|
||||
#define WORDLE_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* Wordle Face
|
||||
* A port of NY Times' Wordle game (https://www.nytimes.com/games/wordle/index.html)
|
||||
* A random 5 letter word is chosen and you have WORDLE_MAX_ATTEMPTS attempts to guess it.
|
||||
* Each guess must be a valid 5-letter word found in _legal_words in the C file.
|
||||
* The only letters used are _valid_letters, also found in the C file.
|
||||
* After a guess, the letters in the correct spot will remain,
|
||||
* and the letters found in the word, but in the incorrect spot will blink.
|
||||
* The screen after the title screen if a new game is started shows the streak of games won in a row.
|
||||
*
|
||||
* If WORDLE_USE_DAILY_STREAK is set to True, then the game can only be played once per day,
|
||||
* and the streak resets to 0 if a day goes by without playing the game.
|
||||
*
|
||||
* Controls:
|
||||
* Light Press
|
||||
* If Playing: Next letter
|
||||
* Else: Next screen
|
||||
* Light Hold
|
||||
* If Playing: Previous letter
|
||||
* Else: Toggle Hard-Mode. This is skipping over letters that have been confirmed
|
||||
* to not be in the word (indicated via 'H' in the top-right)
|
||||
*
|
||||
* Alarm Press
|
||||
* If Playing: If WORDLE_USE_RANDOM_GUESS is set and Light btn held and
|
||||
* (on first letter or already used a random guess)
|
||||
* and first attempt: Use a random 5 letter word with all letters that are different.
|
||||
* Else: Next position
|
||||
* Else: Next screen
|
||||
* Alarm Hold
|
||||
* If Playing: Previous position
|
||||
* Else: None
|
||||
*
|
||||
* Note: Actual Hard Mode in Wordle game is "Any revealed hints must be used in subsequent guesses"
|
||||
* But that came off as clunky UX on the Casio. So instead it only removes unused letters from the keyboard
|
||||
* as that also simplifies the keyboard.
|
||||
*/
|
||||
|
||||
#define WORDLE_LENGTH 5
|
||||
#define WORDLE_MAX_ATTEMPTS 6
|
||||
/* WORDLE_USE_DAILY_STREAK
|
||||
* 0 = Don't ever reset the streak or the puzzle.
|
||||
* 1 = Reset the streak and puzzle 24hrs after starting a puzzle and not finishing it.
|
||||
* If the last puzzle was started at 8AM, it'll be considered failed at 8AM the next day.
|
||||
* 2 = Reset the streak and puzzle if a puzzle goes unsolved or not started a day after the previous one.
|
||||
* If the last puzzle was started at 8AM, it'll be considered failed at midnight the next day.
|
||||
* This will not be the case if the puzzle is started at 8AM, continued at 11:59PM and solved at 12:01AM, the game will let that slide.
|
||||
* Starting a new game instead of continuing is not allowed in this state.
|
||||
*/
|
||||
#define WORDLE_USE_DAILY_STREAK 1
|
||||
#define WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES false // This allows non-words to be entered and repeat guesses to be made. It saves ~11.5KB of ROM.
|
||||
/* WORDLE_USE_RANDOM_GUESS
|
||||
* 0 = Don't allow quickly choosing a random quess
|
||||
* 1 = Allow using a random guess of any value that can be an answer
|
||||
* 2 = Allow using a random guess of any value that can be an answer where all of its letters are unique
|
||||
* 3 = Allow using a random guess of any value that can be an answer, and it's considered one of the best initial choices.
|
||||
*/
|
||||
#define WORDLE_USE_RANDOM_GUESS 2
|
||||
#include "wordle_face_dict.h"
|
||||
|
||||
#define WORDLE_NUM_WORDS (sizeof(_valid_words) / sizeof(_valid_words[0]))
|
||||
#define WORDLE_NUM_POSSIBLE_WORDS (sizeof(_possible_words) / sizeof(_possible_words[0]))
|
||||
#define WORDLE_NUM_VALID_LETTERS (sizeof(_valid_letters) / sizeof(_valid_letters[0]))
|
||||
|
||||
typedef enum {
|
||||
WORDLE_LETTER_WRONG = 0,
|
||||
WORDLE_LETTER_WRONG_LOC,
|
||||
WORDLE_LETTER_CORRECT,
|
||||
WORDLE_LETTER_COUNT
|
||||
} WordleLetterResult;
|
||||
|
||||
typedef enum {
|
||||
SCREEN_TITLE = 0,
|
||||
SCREEN_STREAK,
|
||||
SCREEN_CONTINUE,
|
||||
#if WORDLE_USE_DAILY_STREAK
|
||||
SCREEN_WAIT,
|
||||
#endif
|
||||
SCREEN_PLAYING,
|
||||
SCREEN_RESULT,
|
||||
SCREEN_WIN,
|
||||
SCREEN_LOSE,
|
||||
SCREEN_NO_DICT,
|
||||
SCREEN_ALREADY_GUESSED,
|
||||
SCREEN_COUNT
|
||||
} WordleScreen;
|
||||
|
||||
typedef struct {
|
||||
// Anything you need to keep track of, put it here!
|
||||
uint8_t word_elements[WORDLE_LENGTH];
|
||||
WordleLetterResult word_elements_result[WORDLE_LENGTH];
|
||||
#if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES
|
||||
uint16_t guessed_words[WORDLE_MAX_ATTEMPTS];
|
||||
#endif
|
||||
uint8_t attempt : 4;
|
||||
uint8_t position : 3;
|
||||
bool using_random_guess : 1;
|
||||
uint16_t curr_answer : 14;
|
||||
bool continuing : 1;
|
||||
bool skip_wrong_letter : 1;
|
||||
uint8_t streak;
|
||||
WordleScreen curr_screen;
|
||||
bool known_wrong_letters[WORDLE_NUM_VALID_LETTERS];
|
||||
uint32_t day_last_game_started;
|
||||
} wordle_state_t;
|
||||
|
||||
void wordle_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void wordle_face_activate(movement_settings_t *settings, void *context);
|
||||
bool wordle_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void wordle_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define wordle_face ((const watch_face_t){ \
|
||||
wordle_face_setup, \
|
||||
wordle_face_activate, \
|
||||
wordle_face_loop, \
|
||||
wordle_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // WORDLE_FACE_H_
|
||||
|
293
movement/watch_faces/complication/wordle_face_dict.h
Normal file
293
movement/watch_faces/complication/wordle_face_dict.h
Normal file
|
@ -0,0 +1,293 @@
|
|||
#ifndef WORDLE_FACE_DICT_H_
|
||||
#define WORDLE_FACE_DICT_H_
|
||||
|
||||
#ifndef WORDLE_LENGTH
|
||||
#define WORDLE_LENGTH 5
|
||||
#endif
|
||||
|
||||
#ifndef WORDLE_USE_RANDOM_GUESS
|
||||
#define WORDLE_USE_RANDOM_GUESS 2
|
||||
#endif
|
||||
|
||||
static const char _valid_letters[] = {'A', 'C', 'E', 'H', 'I', 'L', 'N', 'O', 'P', 'R', 'S', 'T'};
|
||||
|
||||
// From: https://matthewminer.name/projects/calculators/wordle-words-left/
|
||||
// Number of words found: 432
|
||||
static const char _valid_words[][WORDLE_LENGTH + 1] = {
|
||||
"SLATE", "STARE", "SNARE", "SANER", "CRANE", "STALE", "CRATE", "RAISE", "TRACE",
|
||||
"SHARE", "ARISE", "SCARE", "SPARE", "CHAOS", "TAPIR", "CAIRN", "TENOR", "CLEAN",
|
||||
"HEART", "SCOPE", "SNARL", "SLEPT", "SINCE", "EPOCH", "SPACE", "RELIC", "SPOIL",
|
||||
"LITER", "LEAPT", "LANCE", "RANCH", "HORSE", "LEACH", "LATER", "STEAL", "CHEAP",
|
||||
"SHORT", "ETHIC", "CHANT", "ACTOR", "REACH", "SEPIA", "ONSET", "SPLAT", "LEANT",
|
||||
"REACT", "OCTAL", "SPORE", "IRATE", "CORAL", "NICER", "SPILT", "SCENT", "PANIC",
|
||||
"SHIRT", "PECAN", "SLAIN", "SPLIT", "ROACH", "ASCOT", "PHONE", "LITHE", "STOIC",
|
||||
"STRIP", "RENAL", "POISE", "ENACT", "CHEAT", "PITCH", "NOISE", "INLET", "PEARL",
|
||||
"POLAR", "PEACH", "STOLE", "CASTE", "CREST", "CRONE", "ETHOS", "THEIR", "STONE",
|
||||
"SHIRE", "LATCH", "HASTE", "CLOSE", "SPINE", "SLANT", "SPEAR", "SCALE", "CAPER",
|
||||
"RETCH", "PESTO", "CHIRP", "SPORT", "OPTIC", "SNAIL", "PRICE", "PLANE", "TORCH",
|
||||
"PASTE", "RECAP", "SOLAR", "CRASH", "LINER", "OPINE", "ASHEN", "PALER", "ECLAT",
|
||||
"SPELT", "TRIAL", "PERIL", "SLICE", "SCANT", "SAINT", "POSIT", "ATONE", "SPIRE",
|
||||
"COAST", "INEPT", "SHOAL", "CLASH", "THORN", "PHASE", "SCORE", "TRICE", "PERCH",
|
||||
"PORCH", "SHEAR", "CHOIR", "RHINO", "PLANT", "SHONE", "CHORE", "LEARN", "ALTER",
|
||||
"CHAIN", "PANEL", "PLIER", "STEIN", "COPSE", "SONIC", "ALIEN", "CHOSE", "ACORN",
|
||||
"ANTIC", "CHEST", "OTHER", "CHINA", "TALON", "SCORN", "PLAIN", "PILOT", "RIPEN",
|
||||
"PATCH", "SPICE", "CLONE", "SCION", "SCONE", "STRAP", "PARSE", "SHALE", "RISEN",
|
||||
"CANOE", "INTER", "LEASH", "ISLET", "PRINT", "SHINE", "NORTH", "CLEAT", "PLAIT",
|
||||
"SCRAP", "CLEAR", "SLOTH", "LAPSE", "CHAIR", "SNORT", "SHARP", "OPERA", "STAIN",
|
||||
"TEACH", "TRAIL", "TRAIN", "LATHE", "PIANO", "PINCH", "PETAL", "STERN", "PRONE",
|
||||
"PROSE", "PLEAT", "TROPE", "PLACE", "POSER", "INERT", "CHASE", "CAROL", "STAIR",
|
||||
"SATIN", "SPITE", "LOATH", "ROAST", "ARSON", "SHAPE", "CLASP", "LOSER", "SALON",
|
||||
"CATER", "SHALT", "INTRO", "ALERT", "PENAL", "SHORE", "RINSE", "CREPT", "APRON",
|
||||
"SONAR", "AISLE", "AROSE", "HATER", "NICHE", "POINT", "EARTH", "PINTO", "THOSE",
|
||||
"CLOTH", "NOTCH", "TOPIC", "RESIN", "SCALP", "HEIST", "HERON", "TRIPE", "TONAL",
|
||||
"TAPER", "SHORN", "TONIC", "HOIST", "SNORE", "STORE", "SLOPE", "OCEAN", "CHART",
|
||||
"PAINT", "SPENT", "SNIPE", "CRISP", "TRASH", "PATIO", "PLATE", "HOTEL", "LEAST",
|
||||
"ALONE", "RALPH", "SPIEL", "SIREN", "RATIO", "STOOP", "TROLL", "ATOLL", "SLASH",
|
||||
"RETRO", "CREEP", "STILT", "SPREE", "TASTE", "CACHE", "CANON", "EATEN", "TEPEE",
|
||||
"SHEET", "SNEER", "ERROR", "NATAL", "SLEEP", "STINT", "TROOP", "SHALL", "STALL",
|
||||
"PIPER", "TOAST", "NASAL", "CORER", "THERE", "POOCH", "SCREE", "ELITE", "ALTAR",
|
||||
"PENCE", "EATER", "ALPHA", "TENTH", "LINEN", "SHEER", "TAINT", "HEATH", "CRIER",
|
||||
"TENSE", "CARAT", "CANAL", "APNEA", "THESE", "HATCH", "SHELL", "CIRCA", "APART",
|
||||
"SPILL", "STEEL", "LOCAL", "STOOL", "SHEEN", "RESET", "STEEP", "ELATE", "PRESS",
|
||||
"SLEET", "CROSS", "TOTAL", "TREAT", "ONION", "STATE", "CINCH", "ASSET", "THREE",
|
||||
"TORSO", "SNOOP", "PENNE", "SPOON", "SHEEP", "PAPAL", "STILL", "CHILL", "THETA",
|
||||
"LEECH", "INNER", "HONOR", "LOOSE", "CONIC", "SCENE", "COACH", "CONCH", "LATTE",
|
||||
"ERASE", "ESTER", "PEACE", "PASTA", "INANE", "SPOOL", "TEASE", "HARSH", "PIECE",
|
||||
"STEER", "SCOOP", "NINTH", "OTTER", "OCTET", "EERIE", "RISER", "LAPEL", "HIPPO",
|
||||
"PREEN", "ETHER", "AORTA", "SENSE", "TRACT", "SHOOT", "SLOOP", "REPEL", "TITHE",
|
||||
"IONIC", "CELLO", "CHESS", "SOOTH", "COCOA", "TITAN", "TOOTH", "TIARA", "CRESS",
|
||||
"SLOSH", "RARER", "TERSE", "ERECT", "HELLO", "PARER", "RIPER", "NOOSE", "CREPE",
|
||||
"CACAO", "ILIAC", "POSSE", "CACTI", "EASEL", "LASSO", "ROOST", "ALLOT", "COLON",
|
||||
"LEPER", "TEETH", "TITLE", "HENCE", "NIECE", "PAPER", "TRITE", "SPELL", "RACER",
|
||||
"ATTIC", "CRASS", "HITCH", "LEASE", "CEASE", "ROTOR", "ELOPE", "APPLE", "CHILI",
|
||||
"START", "PHOTO", "SALSA", "STASH", "PRIOR", "TAROT", "COLOR", "CHEER", "CLASS",
|
||||
"ARENA", "ELECT", "ENTER", "CATCH", "TENET", "TACIT", "TRAIT", "TERRA", "LILAC",
|
||||
};
|
||||
|
||||
// These are words that'll never be used, but still need to be in the dictionary for guesses.
|
||||
// Number of words found: 1898
|
||||
static const char _possible_words[][WORDLE_LENGTH + 1] = {
|
||||
#if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES
|
||||
"AALII", "AARTI", "ACAIS", "ACARI", "ACCAS", "ACERS", "ACETA", "ACHAR", "ACHES",
|
||||
"ACHOO", "ACINI", "ACNES", "ACRES", "ACROS", "ACTIN", "ACTON", "AECIA", "AEONS",
|
||||
"AERIE", "AEROS", "AESIR", "AHEAP", "AHENT", "AHINT", "AINEE", "AIOLI", "AIRER",
|
||||
"AIRNS", "AIRTH", "AIRTS", "AITCH", "ALAAP", "ALANE", "ALANS", "ALANT", "ALAPA",
|
||||
"ALAPS", "ALATE", "ALCOS", "ALECS", "ALEPH", "ALIAS", "ALINE", "ALIST", "ALLEE",
|
||||
"ALLEL", "ALLIS", "ALOES", "ALOHA", "ALOIN", "ALOOS", "ALTHO", "ALTOS", "ANANA",
|
||||
"ANATA", "ANCHO", "ANCLE", "ANCON", "ANEAR", "ANELE", "ANENT", "ANILE", "ANILS",
|
||||
"ANION", "ANISE", "ANLAS", "ANNAL", "ANNAS", "ANNAT", "ANOAS", "ANOLE", "ANSAE",
|
||||
"ANTAE", "ANTAR", "ANTAS", "ANTES", "ANTIS", "ANTRA", "ANTRE", "APACE", "APERS",
|
||||
"APERT", "APHIS", "APIAN", "APIOL", "APISH", "APOOP", "APORT", "APPAL", "APPEL",
|
||||
"APPRO", "APRES", "APSES", "APSIS", "APSOS", "APTER", "ARARS", "ARCHI", "ARCOS",
|
||||
"AREAE", "AREAL", "AREAR", "AREAS", "ARECA", "AREIC", "ARENE", "AREPA", "ARERE",
|
||||
"ARETE", "ARETS", "ARETT", "ARHAT", "ARIAS", "ARIEL", "ARILS", "ARIOT", "ARISH",
|
||||
"ARLES", "ARNAS", "AROHA", "ARPAS", "ARPEN", "ARRAH", "ARRAS", "ARRET", "ARRIS",
|
||||
"ARSES", "ARSIS", "ARTAL", "ARTEL", "ARTIC", "ARTIS", "ASANA", "ASCON", "ASHES",
|
||||
"ASHET", "ASPEN", "ASPER", "ASPIC", "ASPIE", "ASPIS", "ASPRO", "ASSAI", "ASSES",
|
||||
"ASSOT", "ASTER", "ASTIR", "ATAPS", "ATILT", "ATLAS", "ATOCS", "ATRIA", "ATRIP",
|
||||
"ATTAP", "ATTAR", "CACAS", "CAECA", "CAESE", "CAINS", "CALLA", "CALLS", "CALOS",
|
||||
"CALPA", "CALPS", "CANEH", "CANER", "CANES", "CANNA", "CANNS", "CANSO", "CANST",
|
||||
"CANTO", "CANTS", "CAPAS", "CAPES", "CAPHS", "CAPLE", "CAPON", "CAPOS", "CAPOT",
|
||||
"CAPRI", "CARAP", "CARER", "CARES", "CARET", "CARLE", "CARLS", "CARNS", "CARON",
|
||||
"CARPI", "CARPS", "CARRS", "CARSE", "CARTA", "CARTE", "CARTS", "CASAS", "CASCO",
|
||||
"CASES", "CASTS", "CATES", "CECAL", "CEILI", "CEILS", "CELLA", "CELLI", "CELLS",
|
||||
"CELTS", "CENSE", "CENTO", "CENTS", "CEORL", "CEPES", "CERCI", "CERES", "CERIA",
|
||||
"CERIC", "CERNE", "CEROC", "CEROS", "CERTS", "CESSE", "CESTA", "CESTI", "CETES",
|
||||
"CHACE", "CHACO", "CHAIS", "CHALS", "CHANA", "CHAPE", "CHAPS", "CHAPT", "CHARA",
|
||||
"CHARE", "CHARR", "CHARS", "CHATS", "CHEEP", "CHELA", "CHELP", "CHERE", "CHERT",
|
||||
"CHETH", "CHIAO", "CHIAS", "CHICA", "CHICH", "CHICO", "CHICS", "CHIEL", "CHILE",
|
||||
"CHINE", "CHINO", "CHINS", "CHIPS", "CHIRL", "CHIRO", "CHIRR", "CHIRT", "CHITS",
|
||||
"CHOCO", "CHOCS", "CHOIL", "CHOLA", "CHOLI", "CHOLO", "CHONS", "CHOON", "CHOPS",
|
||||
"CHOTA", "CHOTT", "CIELS", "CILIA", "CILLS", "CINCT", "CINES", "CIONS", "CIPPI",
|
||||
"CIRCS", "CIRES", "CIRLS", "CIRRI", "CISCO", "CISTS", "CITAL", "CITER", "CITES",
|
||||
"CLACH", "CLAES", "CLANS", "CLAPS", "CLAPT", "CLARO", "CLART", "CLAST", "CLATS",
|
||||
"CLEEP", "CLEPE", "CLEPT", "CLIES", "CLINE", "CLINT", "CLIPE", "CLIPS", "CLIPT",
|
||||
"CLITS", "CLONS", "CLOOP", "CLOOT", "CLOPS", "CLOTE", "CLOTS", "COACT", "COALA",
|
||||
"COALS", "COAPT", "COATE", "COATI", "COATS", "COCAS", "COCCI", "COCCO", "COCOS",
|
||||
"COHEN", "COHOE", "COHOS", "COILS", "COINS", "COIRS", "COITS", "COLAS", "COLES",
|
||||
"COLIC", "COLIN", "COLLS", "COLTS", "CONES", "CONIA", "CONIN", "CONNE", "CONNS",
|
||||
"CONTE", "CONTO", "COOCH", "COOEE", "COOER", "COOLS", "COONS", "COOPS", "COOPT",
|
||||
"COOST", "COOTS", "COPAL", "COPEN", "COPER", "COPES", "COPRA", "CORES", "CORIA",
|
||||
"CORNI", "CORNO", "CORNS", "CORPS", "CORSE", "CORSO", "COSEC", "COSES", "COSET",
|
||||
"COSIE", "COSTA", "COSTE", "COSTS", "COTAN", "COTES", "COTHS", "COTTA", "COTTS",
|
||||
"CRAAL", "CRAIC", "CRANS", "CRAPE", "CRAPS", "CRARE", "CREEL", "CREES", "CRENA",
|
||||
"CREPS", "CRIAS", "CRIES", "CRINE", "CRIOS", "CRIPE", "CRIPS", "CRISE", "CRITH",
|
||||
"CRITS", "CROCI", "CROCS", "CRONS", "CROOL", "CROON", "CROPS", "CRORE", "CROST",
|
||||
"CTENE", "EALES", "EARLS", "EARNS", "EARNT", "EARST", "EASER", "EASES", "EASLE",
|
||||
"EASTS", "EATHE", "ECHES", "ECHOS", "EISEL", "ELAIN", "ELANS", "ELCHI", "ELINT",
|
||||
"ELOIN", "ELOPS", "ELPEE", "ELSIN", "ENATE", "ENIAC", "ENLIT", "ENOLS", "ENROL",
|
||||
"ENTIA", "EORLS", "EOSIN", "EPACT", "EPEES", "EPHAH", "EPHAS", "EPHOR", "EPICS",
|
||||
"EPOPT", "EPRIS", "ERICA", "ERICS", "ERNES", "EROSE", "ERSES", "ESCAR", "ESCOT",
|
||||
"ESILE", "ESNES", "ESSES", "ESTOC", "ESTOP", "ESTRO", "ETAPE", "ETATS", "ETENS",
|
||||
"ETHAL", "ETHNE", "ETICS", "ETNAS", "ETTIN", "ETTLE", "HAARS", "HAETS", "HAHAS",
|
||||
"HAILS", "HAINS", "HAINT", "HAIRS", "HAITH", "HALAL", "HALER", "HALES", "HALLO",
|
||||
"HALLS", "HALON", "HALOS", "HALSE", "HALTS", "HANAP", "HANCE", "HANCH", "HANSA",
|
||||
"HANSE", "HANTS", "HAOLE", "HAPPI", "HARES", "HARLS", "HARNS", "HAROS", "HARPS",
|
||||
"HARTS", "HASPS", "HASTA", "HATES", "HATHA", "HEALS", "HEAPS", "HEARE", "HEARS",
|
||||
"HEAST", "HEATS", "HECHT", "HEELS", "HEILS", "HEIRS", "HELES", "HELIO", "HELLS",
|
||||
"HELOS", "HELOT", "HELPS", "HENCH", "HENNA", "HENTS", "HEPAR", "HERES", "HERLS",
|
||||
"HERNS", "HEROS", "HERSE", "HESPS", "HESTS", "HETES", "HETHS", "HIANT", "HILAR",
|
||||
"HILCH", "HILLO", "HILLS", "HILTS", "HINTS", "HIOIS", "HIREE", "HIRER", "HIRES",
|
||||
"HISTS", "HITHE", "HOARS", "HOAST", "HOERS", "HOISE", "HOLES", "HOLLA", "HOLLO",
|
||||
"HOLON", "HOLOS", "HOLTS", "HONAN", "HONER", "HONES", "HOOCH", "HOONS", "HOOPS",
|
||||
"HOORS", "HOOSH", "HOOTS", "HOPER", "HOPES", "HORAH", "HORAL", "HORAS", "HORIS",
|
||||
"HORNS", "HORST", "HOSEL", "HOSEN", "HOSER", "HOSES", "HOSTA", "HOSTS", "HOTCH",
|
||||
"HOTEN", "ICERS", "ICHES", "ICHOR", "ICIER", "ICONS", "ICTAL", "ICTIC", "ILEAC",
|
||||
"ILEAL", "ILIAL", "ILLER", "ILLTH", "INAPT", "INCEL", "INCLE", "INION", "INNIT",
|
||||
"INSET", "INSPO", "INTEL", "INTIL", "INTIS", "INTRA", "IOTAS", "IPPON", "IRONE",
|
||||
"IRONS", "ISHES", "ISLES", "ISNAE", "ISSEI", "ISTLE", "ITHER", "LAARI", "LACER",
|
||||
"LACES", "LACET", "LAERS", "LAHAL", "LAHAR", "LAICH", "LAICS", "LAIRS", "LAITH",
|
||||
"LALLS", "LANAI", "LANAS", "LANCH", "LANES", "LANTS", "LAPIN", "LAPIS", "LARCH",
|
||||
"LAREE", "LARES", "LARIS", "LARNS", "LARNT", "LASER", "LASES", "LASSI", "LASTS",
|
||||
"LATAH", "LATEN", "LATHI", "LATHS", "LEANS", "LEAPS", "LEARE", "LEARS", "LEATS",
|
||||
"LEEAR", "LEEPS", "LEERS", "LEESE", "LEETS", "LEHRS", "LEIRS", "LEISH", "LENES",
|
||||
"LENIS", "LENOS", "LENSE", "LENTI", "LENTO", "LEONE", "LEPRA", "LEPTA", "LERES",
|
||||
"LERPS", "LESES", "LESTS", "LETCH", "LETHE", "LIANA", "LIANE", "LIARS", "LIART",
|
||||
"LICHI", "LICHT", "LICIT", "LIENS", "LIERS", "LILLS", "LILOS", "LILTS", "LINAC",
|
||||
"LINCH", "LINES", "LININ", "LINNS", "LINOS", "LINTS", "LIONS", "LIPAS", "LIPES",
|
||||
"LIPIN", "LIPOS", "LIRAS", "LIROT", "LISLE", "LISPS", "LISTS", "LITAI", "LITAS",
|
||||
"LITES", "LITHO", "LITHS", "LITRE", "LLANO", "LOACH", "LOANS", "LOAST", "LOCHE",
|
||||
"LOCHS", "LOCIE", "LOCIS", "LOCOS", "LOESS", "LOHAN", "LOINS", "LOIPE", "LOIRS",
|
||||
"LOLLS", "LONER", "LOOIE", "LOONS", "LOOPS", "LOOTS", "LOPER", "LOPES", "LORAL",
|
||||
"LORAN", "LOREL", "LORES", "LORIC", "LORIS", "LOSEL", "LOSEN", "LOSES", "LOTAH",
|
||||
"LOTAS", "LOTES", "LOTIC", "LOTOS", "LOTSA", "LOTTA", "LOTTE", "LOTTO", "NAANS",
|
||||
"NACHE", "NACHO", "NACRE", "NAHAL", "NAILS", "NAIRA", "NALAS", "NALLA", "NANAS",
|
||||
"NANCE", "NANNA", "NANOS", "NAPAS", "NAPES", "NAPOO", "NAPPA", "NAPPE", "NARAS",
|
||||
"NARCO", "NARCS", "NARES", "NARIC", "NARIS", "NARRE", "NASHI", "NATCH", "NATES",
|
||||
"NATIS", "NEALS", "NEAPS", "NEARS", "NEATH", "NEATS", "NEELE", "NEEPS", "NEESE",
|
||||
"NEIST", "NELIS", "NENES", "NEONS", "NEPER", "NEPIT", "NERAL", "NEROL", "NERTS",
|
||||
"NESTS", "NETES", "NETOP", "NETTS", "NICHT", "NICOL", "NIHIL", "NILLS", "NINER",
|
||||
"NINES", "NINON", "NIPAS", "NIRLS", "NISEI", "NISSE", "NITER", "NITES", "NITON",
|
||||
"NITRE", "NITRO", "NOAHS", "NOELS", "NOILS", "NOINT", "NOIRS", "NOLES", "NOLLS",
|
||||
"NOLOS", "NONAS", "NONCE", "NONES", "NONET", "NONIS", "NOOIT", "NOONS", "NOOPS",
|
||||
"NOPAL", "NORIA", "NORIS", "NOSER", "NOSES", "NOTAL", "NOTER", "NOTES", "OASES",
|
||||
"OASIS", "OASTS", "OATEN", "OATER", "OATHS", "OCHER", "OCHES", "OCHRE", "OCREA",
|
||||
"OCTAN", "OCTAS", "OHIAS", "OHONE", "OILER", "OINTS", "OLEIC", "OLEIN", "OLENT",
|
||||
"OLEOS", "OLIOS", "OLLAS", "OLLER", "OLLIE", "OLPAE", "OLPES", "ONCER", "ONCES",
|
||||
"ONCET", "ONERS", "ONTIC", "OONTS", "OORIE", "OOSES", "OPAHS", "OPALS", "OPENS",
|
||||
"OPEPE", "OPPOS", "OPSIN", "OPTER", "ORACH", "ORALS", "ORANT", "ORATE", "ORCAS",
|
||||
"ORCIN", "ORIEL", "ORLES", "ORLON", "ORLOP", "ORNIS", "ORPIN", "ORRIS", "ORTHO",
|
||||
"OSCAR", "OSHAC", "OSIER", "OSSIA", "OSTIA", "OTTAR", "OTTOS", "PAALS", "PAANS",
|
||||
"PACAS", "PACER", "PACES", "PACHA", "PACOS", "PACTA", "PACTS", "PAEAN", "PAEON",
|
||||
"PAILS", "PAINS", "PAIRE", "PAIRS", "PAISA", "PAISE", "PALAS", "PALEA", "PALES",
|
||||
"PALET", "PALIS", "PALLA", "PALLS", "PALPI", "PALPS", "PALSA", "PANCE", "PANES",
|
||||
"PANNE", "PANNI", "PANTO", "PANTS", "PAOLI", "PAOLO", "PAPAS", "PAPES", "PAPPI",
|
||||
"PARAE", "PARAS", "PARCH", "PAREN", "PAREO", "PARES", "PARIS", "PARLE", "PAROL",
|
||||
"PARPS", "PARRA", "PARRS", "PARTI", "PARTS", "PASEO", "PASES", "PASHA", "PASSE",
|
||||
"PASTS", "PATEN", "PATER", "PATES", "PATHS", "PATIN", "PATTE", "PEALS", "PEANS",
|
||||
"PEARE", "PEARS", "PEART", "PEASE", "PEATS", "PECHS", "PEECE", "PEELS", "PEENS",
|
||||
"PEEPE", "PEEPS", "PEERS", "PEINS", "PEISE", "PELAS", "PELES", "PELLS", "PELON",
|
||||
"PELTA", "PELTS", "PENES", "PENIE", "PENIS", "PENNA", "PENNI", "PENTS", "PEONS",
|
||||
"PEPLA", "PEPOS", "PEPSI", "PERAI", "PERCE", "PERCS", "PEREA", "PERES", "PERIS",
|
||||
"PERNS", "PERPS", "PERSE", "PERST", "PERTS", "PESOS", "PESTS", "PETAR", "PETER",
|
||||
"PETIT", "PETRE", "PETRI", "PETTI", "PETTO", "PHARE", "PHEER", "PHENE", "PHEON",
|
||||
"PHESE", "PHIAL", "PHISH", "PHOCA", "PHONO", "PHONS", "PHOTS", "PHPHT", "PIANI",
|
||||
"PIANS", "PICAL", "PICAS", "PICOT", "PICRA", "PIERS", "PIERT", "PIETA", "PIETS",
|
||||
"PILAE", "PILAO", "PILAR", "PILCH", "PILEA", "PILEI", "PILER", "PILES", "PILIS",
|
||||
"PILLS", "PINAS", "PINES", "PINNA", "PINON", "PINOT", "PINTA", "PINTS", "PIONS",
|
||||
"PIPAL", "PIPAS", "PIPES", "PIPET", "PIPIS", "PIPIT", "PIRAI", "PIRLS", "PIRNS",
|
||||
"PISCO", "PISES", "PISOS", "PISTE", "PITAS", "PITHS", "PITON", "PITOT", "PITTA",
|
||||
"PLAAS", "PLANS", "PLAPS", "PLASH", "PLAST", "PLATS", "PLATT", "PLEAS", "PLENA",
|
||||
"PLEON", "PLESH", "PLICA", "PLIES", "PLOAT", "PLOPS", "PLOTS", "POACH", "POEPS",
|
||||
"POETS", "POLER", "POLES", "POLIO", "POLIS", "POLLS", "POLOS", "POLTS", "PONCE",
|
||||
"PONES", "PONTS", "POOHS", "POOLS", "POONS", "POOPS", "POORI", "POORT", "POOTS",
|
||||
"POPES", "POPPA", "PORAE", "PORAL", "PORER", "PORES", "PORIN", "PORNO", "PORNS",
|
||||
"PORTA", "PORTS", "POSES", "POSHO", "POSTS", "POTAE", "POTCH", "POTES", "POTIN",
|
||||
"POTOO", "POTTO", "POTTS", "PRANA", "PRAOS", "PRASE", "PRATE", "PRATS", "PRATT",
|
||||
"PREES", "PRENT", "PREON", "PREOP", "PREPS", "PRESA", "PRESE", "PREST", "PRIAL",
|
||||
"PRIER", "PRIES", "PRILL", "PRION", "PRISE", "PRISS", "PROAS", "PROIN", "PROLE",
|
||||
"PROLL", "PROPS", "PRORE", "PROSO", "PROSS", "PROST", "PROTO", "PSION", "PSOAE",
|
||||
"PSOAI", "PSOAS", "PSORA", "RACES", "RACHE", "RACON", "RAIAS", "RAILE", "RAILS",
|
||||
"RAINE", "RAINS", "RAITA", "RAITS", "RALES", "RANAS", "RANCE", "RANEE", "RANIS",
|
||||
"RANTS", "RAPER", "RAPES", "RAPHE", "RAPPE", "RAREE", "RARES", "RASER", "RASES",
|
||||
"RASPS", "RASSE", "RASTA", "RATAL", "RATAN", "RATAS", "RATCH", "RATEL", "RATER",
|
||||
"RATES", "RATHA", "RATHE", "RATHS", "RATOO", "RATOS", "REAIS", "REALO", "REALS",
|
||||
"REANS", "REAPS", "REARS", "REAST", "REATA", "REATE", "RECAL", "RECCE", "RECCO",
|
||||
"RECIT", "RECON", "RECTA", "RECTI", "RECTO", "REECH", "REELS", "REENS", "REEST",
|
||||
"REINS", "REIST", "RELET", "RELIE", "RELIT", "RELLO", "RENIN", "RENNE", "RENOS",
|
||||
"RENTE", "RENTS", "REOIL", "REPIN", "REPLA", "REPOS", "REPOT", "REPPS", "REPRO",
|
||||
"RERAN", "RESAT", "RESEE", "RESES", "RESIT", "RESTO", "RESTS", "RETIA", "RETIE",
|
||||
"RHEAS", "RHIES", "RHINE", "RHONE", "RIALS", "RIANT", "RIATA", "RICER", "RICES",
|
||||
"RICHT", "RICIN", "RIELS", "RILES", "RILLE", "RILLS", "RINES", "RIOTS", "RIPES",
|
||||
"RIPPS", "RISES", "RISHI", "RISPS", "RITES", "RITTS", "ROANS", "ROARS", "ROATE",
|
||||
"ROHES", "ROILS", "ROINS", "ROIST", "ROLES", "ROLLS", "RONEO", "RONES", "RONIN",
|
||||
"RONNE", "RONTE", "RONTS", "ROONS", "ROOPS", "ROOSA", "ROOSE", "ROOTS", "ROPER",
|
||||
"ROPES", "RORAL", "RORES", "RORIC", "RORIE", "RORTS", "ROSES", "ROSET", "ROSHI",
|
||||
"ROSIN", "ROSIT", "ROSTI", "ROSTS", "ROTAL", "ROTAN", "ROTAS", "ROTCH", "ROTES",
|
||||
"ROTIS", "ROTLS", "ROTON", "ROTOS", "ROTTE", "SACRA", "SAICE", "SAICS", "SAILS",
|
||||
"SAINE", "SAINS", "SAIRS", "SAIST", "SAITH", "SALAL", "SALAT", "SALEP", "SALES",
|
||||
"SALET", "SALIC", "SALLE", "SALOL", "SALOP", "SALPA", "SALPS", "SALSE", "SALTO",
|
||||
"SALTS", "SANES", "SANSA", "SANTO", "SANTS", "SAOLA", "SAPAN", "SAPOR", "SARAN",
|
||||
"SAREE", "SARIN", "SARIS", "SAROS", "SASER", "SASIN", "SASSE", "SATAI", "SATES",
|
||||
"SATIS", "SCAIL", "SCALA", "SCALL", "SCANS", "SCAPA", "SCAPE", "SCAPI", "SCARP",
|
||||
"SCARS", "SCART", "SCATH", "SCATS", "SCATT", "SCEAT", "SCENA", "SCOOT", "SCOPA",
|
||||
"SCOPS", "SCOTS", "SCRAE", "SCRAN", "SCRAT", "SCRIP", "SEALS", "SEANS", "SEARE",
|
||||
"SEARS", "SEASE", "SEATS", "SECCO", "SECHS", "SECTS", "SEELS", "SEEPS", "SEERS",
|
||||
"SEHRI", "SEILS", "SEINE", "SEIRS", "SEISE", "SELAH", "SELES", "SELLA", "SELLE",
|
||||
"SELLS", "SENAS", "SENES", "SENNA", "SENOR", "SENSA", "SENSI", "SENTE", "SENTI",
|
||||
"SENTS", "SEPAL", "SEPIC", "SEPTA", "SEPTS", "SERAC", "SERAI", "SERAL", "SERER",
|
||||
"SERES", "SERIC", "SERIN", "SERON", "SERRA", "SERRE", "SERRS", "SESSA", "SETAE",
|
||||
"SETAL", "SETON", "SETTS", "SHAHS", "SHANS", "SHAPS", "SHARN", "SHASH", "SHCHI",
|
||||
"SHEAL", "SHEAS", "SHEEL", "SHENT", "SHEOL", "SHERE", "SHERO", "SHETS", "SHIAI",
|
||||
"SHIEL", "SHIER", "SHIES", "SHILL", "SHINS", "SHIPS", "SHIRR", "SHIRS", "SHISH",
|
||||
"SHISO", "SHIST", "SHITE", "SHITS", "SHLEP", "SHOAT", "SHOER", "SHOES", "SHOLA",
|
||||
"SHOOL", "SHOON", "SHOOS", "SHOPE", "SHOPS", "SHORL", "SHOTE", "SHOTS", "SHOTT",
|
||||
"SHRIS", "SIALS", "SICES", "SICHT", "SIENS", "SIENT", "SIETH", "SILEN", "SILER",
|
||||
"SILES", "SILLS", "SILOS", "SILTS", "SINES", "SINHS", "SIPES", "SIREE", "SIRES",
|
||||
"SIRIH", "SIRIS", "SIROC", "SIRRA", "SISAL", "SISES", "SISTA", "SISTS", "SITAR",
|
||||
"SITES", "SITHE", "SLAES", "SLANE", "SLAPS", "SLART", "SLATS", "SLEER", "SLIER",
|
||||
"SLIPE", "SLIPS", "SLIPT", "SLISH", "SLITS", "SLOAN", "SLOES", "SLOOT", "SLOPS",
|
||||
"SLOTS", "SNAPS", "SNARS", "SNASH", "SNATH", "SNEAP", "SNEES", "SNELL", "SNIES",
|
||||
"SNIPS", "SNIRT", "SNITS", "SNOEP", "SNOOL", "SNOOT", "SNOTS", "SOAPS", "SOARE",
|
||||
"SOARS", "SOCAS", "SOCES", "SOCLE", "SOILS", "SOLAH", "SOLAN", "SOLAS", "SOLEI",
|
||||
"SOLER", "SOLES", "SOLON", "SOLOS", "SONCE", "SONES", "SONNE", "SONSE", "SOOLE",
|
||||
"SOOLS", "SOOPS", "SOOTE", "SOOTS", "SOPHS", "SOPOR", "SOPRA", "SORAL", "SORAS",
|
||||
"SOREE", "SOREL", "SORER", "SORES", "SORNS", "SORRA", "SORTA", "SORTS", "SOTHS",
|
||||
"SOTOL", "SPAER", "SPAES", "SPAHI", "SPAIL", "SPAIN", "SPAIT", "SPALE", "SPALL",
|
||||
"SPALT", "SPANE", "SPANS", "SPARS", "SPART", "SPATE", "SPATS", "SPEAL", "SPEAN",
|
||||
"SPEAT", "SPECS", "SPECT", "SPEEL", "SPEER", "SPEIL", "SPEIR", "SPEOS", "SPETS",
|
||||
"SPIAL", "SPICA", "SPICS", "SPIER", "SPIES", "SPILE", "SPINA", "SPINS", "SPIRT",
|
||||
"SPITS", "SPOOR", "SPOOT", "SPOSH", "SPOTS", "SPRAT", "SPRIT", "STANE", "STAPH",
|
||||
"STAPS", "STARN", "STARR", "STARS", "STATS", "STEAN", "STEAR", "STEEN", "STEIL",
|
||||
"STELA", "STELE", "STELL", "STENO", "STENS", "STENT", "STEPS", "STEPT", "STERE",
|
||||
"STETS", "STICH", "STIES", "STILE", "STIPA", "STIPE", "STIRE", "STIRP", "STIRS",
|
||||
"STOAE", "STOAI", "STOAS", "STOAT", "STOEP", "STOIT", "STOLN", "STONN", "STOOR",
|
||||
"STOPE", "STOPS", "STOPT", "STOSS", "STOTS", "STOTT", "STRAE", "STREP", "STRIA",
|
||||
"STROP", "TAALS", "TAATA", "TACAN", "TACES", "TACET", "TACHE", "TACHO", "TACHS",
|
||||
"TACOS", "TACTS", "TAELS", "TAHAS", "TAHRS", "TAILS", "TAINS", "TAIRA", "TAISH",
|
||||
"TAITS", "TALAR", "TALAS", "TALCS", "TALEA", "TALER", "TALES", "TALLS", "TALPA",
|
||||
"TANAS", "TANHS", "TANNA", "TANTI", "TANTO", "TAPAS", "TAPEN", "TAPES", "TAPET",
|
||||
"TAPIS", "TAPPA", "TARAS", "TARES", "TARNS", "TAROC", "TAROS", "TARPS", "TARRE",
|
||||
"TARSI", "TARTS", "TASAR", "TASER", "TASES", "TASSA", "TASSE", "TASSO", "TATAR",
|
||||
"TATER", "TATES", "TATHS", "TATIE", "TATTS", "TEALS", "TEARS", "TEATS", "TECHS",
|
||||
"TECTA", "TEELS", "TEENE", "TEENS", "TEERS", "TEHRS", "TEILS", "TEINS", "TELAE",
|
||||
"TELCO", "TELES", "TELIA", "TELIC", "TELLS", "TELOI", "TELOS", "TENCH", "TENES",
|
||||
"TENIA", "TENNE", "TENNO", "TENON", "TENTS", "TEPAL", "TEPAS", "TERAI", "TERAS",
|
||||
"TERCE", "TERES", "TERNE", "TERNS", "TERTS", "TESLA", "TESTA", "TESTE", "TESTS",
|
||||
"TETES", "TETHS", "TETRA", "TETRI", "THALE", "THALI", "THANA", "THANE", "THANS",
|
||||
"THARS", "THECA", "THEES", "THEIC", "THEIN", "THENS", "THESP", "THETE", "THILL",
|
||||
"THINE", "THINS", "THIOL", "THIRL", "THOLE", "THOLI", "THORO", "THORP", "THRAE",
|
||||
"THRIP", "THROE", "TIANS", "TIARS", "TICAL", "TICCA", "TICES", "TIERS", "TILER",
|
||||
"TILES", "TILLS", "TILTH", "TILTS", "TINAS", "TINCT", "TINEA", "TINES", "TINTS",
|
||||
"TIPIS", "TIRES", "TIRLS", "TIROS", "TIRRS", "TITCH", "TITER", "TITIS", "TITRE",
|
||||
"TOCOS", "TOEAS", "TOHOS", "TOILE", "TOILS", "TOISE", "TOITS", "TOLAN", "TOLAR",
|
||||
"TOLAS", "TOLES", "TOLLS", "TOLTS", "TONER", "TONES", "TONNE", "TOOLS", "TOONS",
|
||||
"TOOTS", "TOPEE", "TOPER", "TOPES", "TOPHE", "TOPHI", "TOPHS", "TOPIS", "TOPOI",
|
||||
"TOPOS", "TORAH", "TORAN", "TORAS", "TORCS", "TORES", "TORIC", "TORII", "TOROS",
|
||||
"TOROT", "TORRS", "TORSE", "TORSI", "TORTA", "TORTE", "TORTS", "TOSAS", "TOSES",
|
||||
"TOTER", "TOTES", "TRANS", "TRANT", "TRAPE", "TRAPS", "TRAPT", "TRASS", "TRATS",
|
||||
"TRATT", "TREEN", "TREES", "TRESS", "TREST", "TRETS", "TRIAC", "TRIER", "TRIES",
|
||||
"TRILL", "TRINE", "TRINS", "TRIOL", "TRIOR", "TRIOS", "TRIPS", "TRIST", "TROAT",
|
||||
"TROIS", "TRONA", "TRONC", "TRONE", "TRONS", "TROTH", "TROTS", "TSARS",
|
||||
#endif
|
||||
};
|
||||
|
||||
#if (WORDLE_USE_RANDOM_GUESS == 3)
|
||||
static const uint16_t _num_random_guess_words = 13; // The valid_words array begins with this many words that are considered the top 3% best options.
|
||||
#elif (WORDLE_USE_RANDOM_GUESS == 2)
|
||||
static const uint16_t _num_random_guess_words = 257; // The valid_words array begins with this many words where each letter is different.
|
||||
#elif (WORDLE_USE_RANDOM_GUESS == 1)
|
||||
static const uint16_t _num_random_guess_words = _num_words;
|
||||
#endif
|
||||
|
||||
#endif // WORDLE_FACE_DICT_H_
|
249
movement/watch_faces/demo/beeps_face.c
Normal file
249
movement/watch_faces/demo/beeps_face.c
Normal file
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Wesley
|
||||
*
|
||||
* 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 "beeps_face.h"
|
||||
|
||||
void beeps_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(beeps_state_t));
|
||||
memset(*context_ptr, 0, sizeof(beeps_state_t));
|
||||
// Do any one-time tasks in here; the inside of this conditional happens only at boot.
|
||||
}
|
||||
}
|
||||
|
||||
void beeps_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
static void _beep_face_update_lcd(beeps_state_t *state) {
|
||||
char buf[11];
|
||||
const char buzzernote[][7] = {" 5500", " 5827", " 6174"," 6541"," 6930"," 7342"," 7778"," 8241"," 8731"," 9250"," 9800"," 10383"," 11000"," 11654"," 12347"," 13081"," 13859"," 14683"," 15556"," 16481"," 17461"," 18500"," 19600"," 20765"," 22000"," 23308"," 24694"," 26163"," 27718"," 29366"," 31113"," 32963"," 34923"," 36999"," 39200"," 41530"," 44000"," 46616"," 49388"," 52325"," 55437"," 58733"," 62225"," 65925"," 69846"," 73999"," 78399"," 83061"," 88000"," 93233"," 98777"," 104650"," 110873"," 117466"," 124451"," 131851"," 139691"," 147998"," 156798"," 166122"," 176000"," 186466"," 197553"," 209300"," 221746"," 234932"," 248902"," 263702"," 279383"," 295996"," 313596"," 332244"," 352000"," 372931"," 395107"," 418601"," 443492"," 469863"," 497803"," 527404"," 558765"," 591991"," 627193"," 664488"," 704000"," 745862"," 790213"};
|
||||
sprintf(buf, "HZ %s", buzzernote[state->frequency]);
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
bool beeps_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
beeps_state_t *state = (beeps_state_t *)context;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
_beep_face_update_lcd(state);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
state->frequency = (state->frequency + 1) % 87;
|
||||
_beep_face_update_lcd(state);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
if (state->frequency == 0) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A1, 500);
|
||||
} else if (state->frequency == 1) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A1SHARP_B1FLAT, 500);
|
||||
} else if (state->frequency == 2) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_B1, 500);
|
||||
} else if (state->frequency == 3) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C2, 500);
|
||||
} else if (state->frequency == 4) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C2SHARP_D2FLAT, 500);
|
||||
} else if (state->frequency == 5) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D2, 500);
|
||||
} else if (state->frequency == 6) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D2SHARP_E2FLAT, 500);
|
||||
} else if (state->frequency == 7) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E2, 500);
|
||||
} else if (state->frequency == 8) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F2, 500);
|
||||
} else if (state->frequency == 9) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F2SHARP_G2FLAT, 500);
|
||||
} else if (state->frequency == 10) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G2, 500);
|
||||
} else if (state->frequency == 11) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G2SHARP_A2FLAT, 500);
|
||||
} else if (state->frequency == 12) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A2, 500);
|
||||
} else if (state->frequency == 13) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A2SHARP_B2FLAT, 500);
|
||||
} else if (state->frequency == 14) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_B2, 500);
|
||||
} else if (state->frequency == 15) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C3, 500);
|
||||
} else if (state->frequency == 16) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C3SHARP_D3FLAT, 500);
|
||||
} else if (state->frequency == 17) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D3, 500);
|
||||
} else if (state->frequency == 18) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D3SHARP_E3FLAT, 500);
|
||||
} else if (state->frequency == 19) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E3, 500);
|
||||
} else if (state->frequency == 20) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F3, 500);
|
||||
} else if (state->frequency == 21) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F3SHARP_G3FLAT, 500);
|
||||
} else if (state->frequency == 22) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G3, 500);
|
||||
} else if (state->frequency == 23) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G3SHARP_A3FLAT, 500);
|
||||
} else if (state->frequency == 24) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A3, 500);
|
||||
} else if (state->frequency == 25) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A3SHARP_B3FLAT, 500);
|
||||
} else if (state->frequency == 26) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_B3, 500);
|
||||
} else if (state->frequency == 27) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C4, 500);
|
||||
} else if (state->frequency == 28) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C4SHARP_D4FLAT, 500);
|
||||
} else if (state->frequency == 29) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D4, 500);
|
||||
} else if (state->frequency == 30) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D4SHARP_E4FLAT, 500);
|
||||
} else if (state->frequency == 31) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E4, 500);
|
||||
} else if (state->frequency == 32) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F4, 500);
|
||||
} else if (state->frequency == 33) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F4SHARP_G4FLAT, 500);
|
||||
} else if (state->frequency == 34) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G4, 500);
|
||||
} else if (state->frequency == 35) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G4SHARP_A4FLAT, 500);
|
||||
} else if (state->frequency == 36) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A4, 500);
|
||||
} else if (state->frequency == 37) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A4SHARP_B4FLAT, 500);
|
||||
} else if (state->frequency == 38) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_B4, 500);
|
||||
} else if (state->frequency == 39) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C5, 500);
|
||||
} else if (state->frequency == 40) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C5SHARP_D5FLAT, 500);
|
||||
} else if (state->frequency == 41) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D5, 500);
|
||||
} else if (state->frequency == 42) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D5SHARP_E5FLAT, 500);
|
||||
} else if (state->frequency == 43) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E5, 500);
|
||||
} else if (state->frequency == 44) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F5, 500);
|
||||
} else if (state->frequency == 45) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F5SHARP_G5FLAT, 500);
|
||||
} else if (state->frequency == 46) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G5, 500);
|
||||
} else if (state->frequency == 47) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G5SHARP_A5FLAT, 500);
|
||||
} else if (state->frequency == 48) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A5, 500);
|
||||
} else if (state->frequency == 49) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A5SHARP_B5FLAT, 500);
|
||||
} else if (state->frequency == 50) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_B5, 500);
|
||||
} else if (state->frequency == 51) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C6, 500);
|
||||
} else if (state->frequency == 52) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C6SHARP_D6FLAT, 500);
|
||||
} else if (state->frequency == 53) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D6, 500);
|
||||
} else if (state->frequency == 54) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D6SHARP_E6FLAT, 500);
|
||||
} else if (state->frequency == 55) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E6, 500);
|
||||
} else if (state->frequency == 56) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F6, 500);
|
||||
} else if (state->frequency == 57) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F6SHARP_G6FLAT, 500);
|
||||
} else if (state->frequency == 58) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G6, 500);
|
||||
} else if (state->frequency == 59) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G6SHARP_A6FLAT, 500);
|
||||
} else if (state->frequency == 60) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A6, 500);
|
||||
} else if (state->frequency == 61) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A6SHARP_B6FLAT, 500);
|
||||
} else if (state->frequency == 62) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_B6, 500);
|
||||
} else if (state->frequency == 63) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C7, 500);
|
||||
} else if (state->frequency == 64) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C7SHARP_D7FLAT, 500);
|
||||
} else if (state->frequency == 65) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D7, 500);
|
||||
} else if (state->frequency == 66) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D7SHARP_E7FLAT, 500);
|
||||
} else if (state->frequency == 67) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E7, 500);
|
||||
} else if (state->frequency == 68) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F7, 500);
|
||||
} else if (state->frequency == 69) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F7SHARP_G7FLAT, 500);
|
||||
} else if (state->frequency == 70) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G7, 500);
|
||||
} else if (state->frequency == 71) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G7SHARP_A7FLAT, 500);
|
||||
} else if (state->frequency == 72) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A7, 500);
|
||||
} else if (state->frequency == 73) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A7SHARP_B7FLAT, 500);
|
||||
} else if (state->frequency == 74) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_B7, 500);
|
||||
} else if (state->frequency == 75) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C8, 500);
|
||||
} else if (state->frequency == 76) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C8SHARP_D8FLAT, 500);
|
||||
} else if (state->frequency == 77) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D8, 500);
|
||||
} else if (state->frequency == 78) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D8SHARP_E8FLAT, 500);
|
||||
} else if (state->frequency == 79) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E8, 500);
|
||||
} else if (state->frequency == 80) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F8, 500);
|
||||
} else if (state->frequency == 81) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_F8SHARP_G8FLAT, 500);
|
||||
} else if (state->frequency == 82) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G8, 500);
|
||||
} else if (state->frequency == 83) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G8SHARP_A8FLAT, 500);
|
||||
} else if (state->frequency == 84) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A8, 500);
|
||||
} else if (state->frequency == 85) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A8SHARP_B8FLAT, 500);
|
||||
} else if (state->frequency == 86) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_B8, 500);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void beeps_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
61
movement/watch_faces/demo/beeps_face.h
Normal file
61
movement/watch_faces/demo/beeps_face.h
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Wesley
|
||||
*
|
||||
* 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 BEEPS_FACE_H_
|
||||
#define BEEPS_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* A simple watch face to test the different Buzzer Notes.
|
||||
*
|
||||
* Press the Light button to play a sound.
|
||||
* Press the Alarm button to change the frequency.
|
||||
*
|
||||
* The watch face displays the frequency of the buzzer it will play
|
||||
* this allows you to reference the watch_buzzer.h file to find the
|
||||
* corresponding note.
|
||||
*
|
||||
* The watch_buzzer.h file is found at watch-library/shared/watch/watch_buzzer.h
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
uint8_t frequency;
|
||||
} beeps_state_t;
|
||||
|
||||
void beeps_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void beeps_face_activate(movement_settings_t *settings, void *context);
|
||||
bool beeps_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void beeps_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define beeps_face ((const watch_face_t){ \
|
||||
beeps_face_setup, \
|
||||
beeps_face_activate, \
|
||||
beeps_face_loop, \
|
||||
beeps_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // BEEPS_FACE_H_
|
||||
|
|
@ -41,6 +41,7 @@ static void _lis2dw_logging_face_update_display(movement_settings_t *settings, l
|
|||
char time_indication_character;
|
||||
int8_t pos;
|
||||
watch_date_time date_time;
|
||||
bool set_leading_zero = false;
|
||||
|
||||
if (logger_state->log_ticks) {
|
||||
pos = (logger_state->data_points - 1 - logger_state->display_index) % LIS2DW_LOGGING_NUM_DATA_POINTS;
|
||||
|
@ -50,12 +51,14 @@ static void _lis2dw_logging_face_update_display(movement_settings_t *settings, l
|
|||
} else {
|
||||
date_time = logger_state->data[pos].timestamp;
|
||||
watch_set_colon();
|
||||
if (settings->bit.clock_mode_24h) {
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
} else {
|
||||
if (!settings->bit.clock_mode_24h) {
|
||||
if (date_time.unit.hour > 11) watch_set_indicator(WATCH_INDICATOR_PM);
|
||||
date_time.unit.hour %= 12;
|
||||
if (date_time.unit.hour == 0) date_time.unit.hour = 12;
|
||||
} else if (!settings->bit.clock_24h_leading_zero) {
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
} else if (date_time.unit.hour < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
switch (logger_state->axis_index) {
|
||||
case 0:
|
||||
|
@ -89,6 +92,9 @@ static void _lis2dw_logging_face_update_display(movement_settings_t *settings, l
|
|||
logger_state->interrupts[2]);
|
||||
}
|
||||
watch_display_string(buf, 0);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
printf("%s\n", buf);
|
||||
}
|
||||
|
||||
static void _lis2dw_logging_face_log_data(lis2dw_logger_state_t *logger_state) {
|
||||
|
@ -137,7 +143,7 @@ void lis2dw_logging_face_activate(movement_settings_t *settings, void *context)
|
|||
|
||||
logger_state->display_index = 0;
|
||||
logger_state->log_ticks = 0;
|
||||
watch_enable_digital_input(A0);
|
||||
watch_enable_digital_input(A4);
|
||||
}
|
||||
|
||||
bool lis2dw_logging_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
|
@ -191,7 +197,7 @@ bool lis2dw_logging_face_loop(movement_event_t event, movement_settings_t *setti
|
|||
void lis2dw_logging_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
watch_disable_digital_input(A0);
|
||||
watch_disable_digital_input(A4);
|
||||
}
|
||||
|
||||
bool lis2dw_logging_face_wants_background_task(movement_settings_t *settings, void *context) {
|
||||
|
|
162
movement/watch_faces/sensor/accel_interrupt_count_face.c
Normal file
162
movement/watch_faces/sensor/accel_interrupt_count_face.c
Normal file
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Joey Castillo
|
||||
*
|
||||
* 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 "accel_interrupt_count_face.h"
|
||||
#include "lis2dw.h"
|
||||
#include "watch.h"
|
||||
|
||||
// hacky hacky!
|
||||
uint32_t *ptr_to_count = 0;
|
||||
|
||||
void accel_interrupt_handler(void);
|
||||
void accel_interrupt_handler(void) {
|
||||
(*ptr_to_count)++;
|
||||
}
|
||||
|
||||
static void _accel_interrupt_count_face_update_display(accel_interrupt_count_state_t *state) {
|
||||
char buf[11];
|
||||
|
||||
if (state->running) {
|
||||
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
}
|
||||
|
||||
// "AC"celerometer "IN"terrupts
|
||||
snprintf(buf, 11, "AC1N%6ld", state->count);
|
||||
watch_display_string(buf, 0);
|
||||
printf("%s\n", buf);
|
||||
}
|
||||
|
||||
static void _accel_interrupt_count_face_configure_threshold(uint8_t threshold) {
|
||||
lis2dw_configure_wakeup_int1(threshold, false, true);
|
||||
}
|
||||
|
||||
void accel_interrupt_count_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(accel_interrupt_count_state_t));
|
||||
memset(*context_ptr, 0, sizeof(accel_interrupt_count_state_t));
|
||||
ptr_to_count = &((accel_interrupt_count_state_t *)*context_ptr)->count;
|
||||
watch_enable_i2c();
|
||||
lis2dw_begin();
|
||||
lis2dw_set_low_power_mode(LIS2DW_LP_MODE_2); // lowest power 14-bit mode, 25 Hz is 3.5 µA @ 1.8V w/ low noise, 3µA without
|
||||
lis2dw_set_low_noise_mode(true); // consumes a little more power
|
||||
lis2dw_set_range(LIS2DW_CTRL6_VAL_RANGE_4G);
|
||||
lis2dw_set_data_rate(LIS2DW_DATA_RATE_25_HZ); // is this enough?
|
||||
|
||||
// threshold is 1/64th of full scale, so for a FS of ±4G this is 1.25G
|
||||
((accel_interrupt_count_state_t *)*context_ptr)->threshold = 10;
|
||||
_accel_interrupt_count_face_configure_threshold(((accel_interrupt_count_state_t *)*context_ptr)->threshold);
|
||||
}
|
||||
}
|
||||
|
||||
void accel_interrupt_count_face_activate(movement_settings_t *settings, void *context) {
|
||||
accel_interrupt_count_state_t *state = (accel_interrupt_count_state_t *)context;
|
||||
|
||||
// never in settings mode at the start
|
||||
state->is_setting = false;
|
||||
|
||||
// force LE interval to never sleep
|
||||
settings->bit.le_interval = 0;
|
||||
}
|
||||
|
||||
bool accel_interrupt_count_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
accel_interrupt_count_state_t *state = (accel_interrupt_count_state_t *)context;
|
||||
|
||||
if (state->is_setting) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
state->new_threshold = (state->new_threshold + 1) % 64;
|
||||
// fall through
|
||||
case EVENT_TICK:
|
||||
{
|
||||
char buf[11];
|
||||
snprintf(buf, 11, "TH %4d ", state->new_threshold);
|
||||
watch_display_string(buf, 0);
|
||||
printf("%s\n", buf);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
lis2dw_configure_wakeup_int1(state->threshold, false, true);
|
||||
state->threshold = state->new_threshold;
|
||||
state->is_setting = false;
|
||||
break;
|
||||
default:
|
||||
movement_default_loop_handler(event, settings);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (event.event_type) {
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
movement_illuminate_led();
|
||||
|
||||
// if stopped, reset the count
|
||||
if (!state->running) {
|
||||
state->count = 0;
|
||||
}
|
||||
_accel_interrupt_count_face_update_display(state);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
if (state->running) {
|
||||
state->running = false;
|
||||
watch_register_interrupt_callback(A4, NULL, INTERRUPT_TRIGGER_RISING);
|
||||
} else {
|
||||
state->running = true;
|
||||
watch_register_interrupt_callback(A4, accel_interrupt_handler, INTERRUPT_TRIGGER_RISING);
|
||||
}
|
||||
_accel_interrupt_count_face_update_display(state);
|
||||
break;
|
||||
case EVENT_ACTIVATE:
|
||||
case EVENT_TICK:
|
||||
_accel_interrupt_count_face_update_display(state);
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (!state->running) {
|
||||
state->new_threshold = state->threshold;
|
||||
state->is_setting = true;
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
movement_default_loop_handler(event, settings);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void accel_interrupt_count_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
bool accel_interrupt_count_face_wants_background_task(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
return false;
|
||||
}
|
58
movement/watch_faces/sensor/accel_interrupt_count_face.h
Normal file
58
movement/watch_faces/sensor/accel_interrupt_count_face.h
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Joey Castillo
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
* Accelerometer Interrupt Counter
|
||||
*
|
||||
* This is an experimental watch face for counting the number of interrupts that
|
||||
* the Sensor Watch Motion acceleromoeter board fires. I expect it will be removed
|
||||
* once we integrate accelerometer functionality more deeply into Movement.
|
||||
*/
|
||||
|
||||
#include "movement.h"
|
||||
#include "watch.h"
|
||||
|
||||
typedef struct {
|
||||
uint32_t count;
|
||||
uint8_t new_threshold;
|
||||
uint8_t threshold;
|
||||
bool running;
|
||||
bool is_setting;
|
||||
} accel_interrupt_count_state_t;
|
||||
|
||||
void accel_interrupt_count_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void accel_interrupt_count_face_activate(movement_settings_t *settings, void *context);
|
||||
bool accel_interrupt_count_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void accel_interrupt_count_face_resign(movement_settings_t *settings, void *context);
|
||||
bool accel_interrupt_count_face_wants_background_task(movement_settings_t *settings, void *context);
|
||||
|
||||
#define accel_interrupt_count_face ((const watch_face_t){ \
|
||||
accel_interrupt_count_face_setup, \
|
||||
accel_interrupt_count_face_activate, \
|
||||
accel_interrupt_count_face_loop, \
|
||||
accel_interrupt_count_face_resign, \
|
||||
accel_interrupt_count_face_wants_background_task, \
|
||||
})
|
154
movement/watch_faces/sensor/alarm_thermometer_face.c
Normal file
154
movement/watch_faces/sensor/alarm_thermometer_face.c
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Christian Buschau
|
||||
*
|
||||
* 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 <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "alarm_thermometer_face.h"
|
||||
#include "thermistor_driver.h"
|
||||
|
||||
static float _alarm_thermometer_face_update(bool in_fahrenheit) {
|
||||
thermistor_driver_enable();
|
||||
float temperature_c = thermistor_driver_get_temperature();
|
||||
char buf[14];
|
||||
if (in_fahrenheit) {
|
||||
sprintf(buf, "%4.1f#F", temperature_c * 1.8 + 32.0);
|
||||
} else {
|
||||
sprintf(buf, "%4.1f#C", temperature_c);
|
||||
}
|
||||
watch_display_string(buf, 4);
|
||||
thermistor_driver_disable();
|
||||
return temperature_c;
|
||||
}
|
||||
|
||||
static void _alarm_thermometer_face_clear(int last[]) {
|
||||
for (size_t i = 0; i < LAST_SIZE; i++) {
|
||||
last[i] = INT_MIN;
|
||||
}
|
||||
}
|
||||
|
||||
void alarm_thermometer_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(alarm_thermometer_state_t));
|
||||
memset(*context_ptr, 0, sizeof(alarm_thermometer_state_t));
|
||||
}
|
||||
}
|
||||
|
||||
void alarm_thermometer_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
alarm_thermometer_state_t *state = (alarm_thermometer_state_t *)context;
|
||||
state->mode = MODE_NORMAL;
|
||||
_alarm_thermometer_face_clear(state->last);
|
||||
watch_display_string("AT", 0);
|
||||
}
|
||||
|
||||
bool alarm_thermometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
alarm_thermometer_state_t *state = (alarm_thermometer_state_t *)context;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
_alarm_thermometer_face_update(settings->bit.use_imperial_units);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (watch_rtc_get_date_time().unit.second % 5 == 0) {
|
||||
switch (state->mode) {
|
||||
case MODE_NORMAL:
|
||||
_alarm_thermometer_face_update(settings->bit.use_imperial_units);
|
||||
break;
|
||||
case MODE_ALARM:
|
||||
for (size_t i = LAST_SIZE - 1; i > 0; i--) {
|
||||
state->last[i] = state->last[i - 1];
|
||||
}
|
||||
state->last[0] = roundf(_alarm_thermometer_face_update(settings->bit.use_imperial_units) * 10.0f);
|
||||
bool constant = true;
|
||||
for (size_t i = 1; i < LAST_SIZE; i++) {
|
||||
if (state->last[i - 1] != state->last[i]) {
|
||||
constant = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (constant) {
|
||||
state->mode = MODE_FREEZE;
|
||||
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
movement_play_alarm();
|
||||
}
|
||||
break;
|
||||
case MODE_FREEZE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
switch (state->mode) {
|
||||
case MODE_NORMAL:
|
||||
state->mode = MODE_ALARM;
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
_alarm_thermometer_face_clear(state->last);
|
||||
break;
|
||||
case MODE_FREEZE:
|
||||
state->mode = MODE_NORMAL;
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
break;
|
||||
case MODE_ALARM:
|
||||
state->mode = MODE_NORMAL;
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
_alarm_thermometer_face_update(settings->bit.use_imperial_units);
|
||||
break;
|
||||
}
|
||||
if (settings->bit.button_should_sound) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (state->mode != MODE_FREEZE) {
|
||||
settings->bit.use_imperial_units = !settings->bit.use_imperial_units;
|
||||
_alarm_thermometer_face_update(settings->bit.use_imperial_units);
|
||||
}
|
||||
break;
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
if (!watch_tick_animation_is_running()) {
|
||||
state->mode = MODE_NORMAL;
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
watch_start_tick_animation(1000);
|
||||
}
|
||||
if (watch_rtc_get_date_time().unit.minute % 5 == 0) {
|
||||
_alarm_thermometer_face_update(settings->bit.use_imperial_units);
|
||||
watch_display_string(" ", 8);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void alarm_thermometer_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
74
movement/watch_faces/sensor/alarm_thermometer_face.h
Normal file
74
movement/watch_faces/sensor/alarm_thermometer_face.h
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Christian Buschau
|
||||
*
|
||||
* 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 ALARM_THERMOMETER_FACE_H_
|
||||
#define ALARM_THERMOMETER_FACE_H_
|
||||
|
||||
#include <limits.h>
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* ALARM THERMOMETER
|
||||
*
|
||||
* This watch face shows the current temperature in degrees Celsius. Press and
|
||||
* hold the alarm button to toggle between Celsius and Fahrenheit. Press and
|
||||
* release the alarm button to start a "timer". The watch will sound an alarm
|
||||
* when the temperature remains constant for at least 30 seconds and the
|
||||
* temperature will stop updating until you press the alarm button. You can
|
||||
* cancel the alarm by pressing the button again. If the temperature doesn't
|
||||
* remain constant until the low energy timeout is reached, the alarm will stop.
|
||||
* This is useful to measure e.g. the room temperature. If you lay off your
|
||||
* watch from your wrist, it will take some time until it cools down, and will
|
||||
* notify you when the measurement is constant enough.
|
||||
* THIS WATCH FACE IS NOT INTENDED TO DIAGNOSE, TREAT, CURE OR PREVENT ANY
|
||||
* DISEASE.
|
||||
*/
|
||||
|
||||
#define LAST_SIZE 6
|
||||
|
||||
typedef enum {
|
||||
MODE_NORMAL,
|
||||
MODE_ALARM,
|
||||
MODE_FREEZE
|
||||
} alarm_thermometer_mode_t;
|
||||
|
||||
typedef struct {
|
||||
int last[LAST_SIZE];
|
||||
alarm_thermometer_mode_t mode;
|
||||
} alarm_thermometer_state_t;
|
||||
|
||||
void alarm_thermometer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void alarm_thermometer_face_activate(movement_settings_t *settings, void *context);
|
||||
bool alarm_thermometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void alarm_thermometer_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define alarm_thermometer_face ((const watch_face_t){ \
|
||||
alarm_thermometer_face_setup, \
|
||||
alarm_thermometer_face_activate, \
|
||||
alarm_thermometer_face_loop, \
|
||||
alarm_thermometer_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // ALARM_THERMOMETER_FACE_H_
|
154
movement/watch_faces/sensor/minmax_face.c
Normal file
154
movement/watch_faces/sensor/minmax_face.c
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Mark Blyth
|
||||
*
|
||||
* 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 "minmax_face.h"
|
||||
#include "thermistor_driver.h"
|
||||
#include "watch.h"
|
||||
|
||||
|
||||
static float _get_displayed_temperature_c(minmax_state_t *state){
|
||||
float min_temp = 1000;
|
||||
float max_temp = -1000;
|
||||
for(int i = 0; i < LOGGING_DATA_POINTS; i++){
|
||||
if(state->hourly_maxs[i] > max_temp){
|
||||
max_temp = state->hourly_maxs[i];
|
||||
}
|
||||
if(state->hourly_mins[i] < min_temp){
|
||||
min_temp = state->hourly_mins[i];
|
||||
}
|
||||
}
|
||||
if(state->show_min) return min_temp;
|
||||
return max_temp;
|
||||
}
|
||||
|
||||
|
||||
static void _minmax_face_log_data(minmax_state_t *logger_state) {
|
||||
thermistor_driver_enable();
|
||||
size_t pos = (size_t) watch_rtc_get_date_time().unit.hour;
|
||||
float temp_c = thermistor_driver_get_temperature();
|
||||
// If no data yet, initialise with current temperature
|
||||
if(!logger_state->have_logged){
|
||||
logger_state->have_logged = true;
|
||||
for(int i=0; i<LOGGING_DATA_POINTS; i++){
|
||||
logger_state->hourly_mins[i] = temp_c;
|
||||
logger_state->hourly_maxs[i] = temp_c;
|
||||
}
|
||||
}
|
||||
// On new hour, update lists to current temperature
|
||||
else if(watch_rtc_get_date_time().unit.minute < 2){
|
||||
logger_state->hourly_mins[pos] = temp_c;
|
||||
logger_state->hourly_maxs[pos] = temp_c;
|
||||
}
|
||||
// Log hourly highs and lows
|
||||
else if(logger_state->hourly_mins[pos] > temp_c){
|
||||
logger_state->hourly_mins[pos] = temp_c;
|
||||
}
|
||||
else if(logger_state->hourly_maxs[pos] < temp_c){
|
||||
logger_state->hourly_maxs[pos] = temp_c;
|
||||
}
|
||||
thermistor_driver_disable();
|
||||
}
|
||||
|
||||
static void _minmax_face_update_display(float temperature_c, bool in_fahrenheit) {
|
||||
char buf[14];
|
||||
if (in_fahrenheit) {
|
||||
sprintf(buf, "%4.0f#F", temperature_c * 1.8 + 32.0);
|
||||
} else {
|
||||
sprintf(buf, "%4.0f#C", temperature_c);
|
||||
}
|
||||
watch_display_string(buf, 4);
|
||||
}
|
||||
|
||||
|
||||
void minmax_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(minmax_state_t));
|
||||
memset(*context_ptr, 0, sizeof(minmax_state_t));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void minmax_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
minmax_state_t *state = (minmax_state_t *)context;
|
||||
state->show_min = true;
|
||||
watch_display_string("MN", 0); // Start with minimum temp
|
||||
}
|
||||
|
||||
bool minmax_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
minmax_state_t *state = (minmax_state_t *)context;
|
||||
float temp_c;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
temp_c = _get_displayed_temperature_c(state);
|
||||
_minmax_face_update_display(temp_c, settings->bit.use_imperial_units);
|
||||
break;
|
||||
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
settings->bit.use_imperial_units = !settings->bit.use_imperial_units;
|
||||
temp_c = _get_displayed_temperature_c(state);
|
||||
_minmax_face_update_display(temp_c, settings->bit.use_imperial_units);
|
||||
break;
|
||||
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
state->show_min = !state->show_min;
|
||||
if(state->show_min){
|
||||
watch_display_string("MN", 0);
|
||||
} else {
|
||||
watch_display_string("MX", 0);
|
||||
}
|
||||
temp_c = _get_displayed_temperature_c(state);
|
||||
_minmax_face_update_display(temp_c, settings->bit.use_imperial_units);
|
||||
break;
|
||||
|
||||
case EVENT_TIMEOUT:
|
||||
movement_move_to_face(0);
|
||||
break;
|
||||
case EVENT_BACKGROUND_TASK:
|
||||
_minmax_face_log_data(state);
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void minmax_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
|
||||
bool minmax_face_wants_background_task(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
// this will get called at the top of each minute; always request bg task
|
||||
return true;
|
||||
}
|
69
movement/watch_faces/sensor/minmax_face.h
Normal file
69
movement/watch_faces/sensor/minmax_face.h
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Mark Blyth
|
||||
*
|
||||
* 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 MINMAX_FACE_H_
|
||||
#define MINMAX_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
#include "watch.h"
|
||||
|
||||
#define LOGGING_DATA_POINTS (24)
|
||||
|
||||
/*
|
||||
* Log for the min. and max. temperature over the last 24h.
|
||||
*
|
||||
* Temperature is logged once a minute, every minute. Results are
|
||||
* stored, noting the highest and lowest temperatures observed within
|
||||
* any given hour. The watch face then displays the minimum or maximum
|
||||
* temperature recorded over the last 24h.
|
||||
*
|
||||
* A long press of the light button changes units between Celsius and
|
||||
* Fahrenheit. Pressing the alarm button switches between displaying the
|
||||
* minimum and maximum observed temperatures. If no buttons are pressed,
|
||||
* the watch face will eventually time out and return home.
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
bool show_min;
|
||||
bool have_logged;
|
||||
float hourly_mins[LOGGING_DATA_POINTS];
|
||||
float hourly_maxs[LOGGING_DATA_POINTS];
|
||||
} minmax_state_t;
|
||||
|
||||
void minmax_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void minmax_face_activate(movement_settings_t *settings, void *context);
|
||||
bool minmax_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void minmax_face_resign(movement_settings_t *settings, void *context);
|
||||
bool minmax_face_wants_background_task(movement_settings_t *settings, void *context);
|
||||
|
||||
#define minmax_face ((const watch_face_t){ \
|
||||
minmax_face_setup, \
|
||||
minmax_face_activate, \
|
||||
minmax_face_loop, \
|
||||
minmax_face_resign, \
|
||||
minmax_face_wants_background_task, \
|
||||
})
|
||||
|
||||
#endif // MINMAX_FACE_H_
|
||||
|
|
@ -40,9 +40,10 @@ static void _thermistor_logging_face_log_data(thermistor_logger_state_t *logger_
|
|||
thermistor_driver_disable();
|
||||
}
|
||||
|
||||
static void _thermistor_logging_face_update_display(thermistor_logger_state_t *logger_state, bool in_fahrenheit, bool clock_mode_24h) {
|
||||
static void _thermistor_logging_face_update_display(thermistor_logger_state_t *logger_state, bool in_fahrenheit, bool clock_mode_24h, bool clock_24h_leading_zero) {
|
||||
int8_t pos = (logger_state->data_points - 1 - logger_state->display_index) % THERMISTOR_LOGGING_NUM_DATA_POINTS;
|
||||
char buf[14];
|
||||
bool set_leading_zero = false;
|
||||
|
||||
watch_clear_indicator(WATCH_INDICATOR_24H);
|
||||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
|
@ -53,12 +54,14 @@ static void _thermistor_logging_face_update_display(thermistor_logger_state_t *l
|
|||
} else if (logger_state->ts_ticks) {
|
||||
watch_date_time date_time = logger_state->data[pos].timestamp;
|
||||
watch_set_colon();
|
||||
if (clock_mode_24h) {
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
} else {
|
||||
if (!clock_mode_24h) {
|
||||
if (date_time.unit.hour > 11) watch_set_indicator(WATCH_INDICATOR_PM);
|
||||
date_time.unit.hour %= 12;
|
||||
if (date_time.unit.hour == 0) date_time.unit.hour = 12;
|
||||
} else if (!clock_24h_leading_zero) {
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
} else if (date_time.unit.hour < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
sprintf(buf, "AT%2d%2d%02d%02d", date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
|
||||
} else {
|
||||
|
@ -70,6 +73,8 @@ static void _thermistor_logging_face_update_display(thermistor_logger_state_t *l
|
|||
}
|
||||
|
||||
watch_display_string(buf, 0);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
}
|
||||
|
||||
void thermistor_logging_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
|
||||
|
@ -100,18 +105,18 @@ bool thermistor_logging_face_loop(movement_event_t event, movement_settings_t *s
|
|||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
logger_state->ts_ticks = 2;
|
||||
_thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h);
|
||||
_thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h, settings->bit.clock_24h_leading_zero);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
logger_state->display_index = (logger_state->display_index + 1) % THERMISTOR_LOGGING_NUM_DATA_POINTS;
|
||||
logger_state->ts_ticks = 0;
|
||||
// fall through
|
||||
case EVENT_ACTIVATE:
|
||||
_thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h);
|
||||
_thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h, settings->bit.clock_24h_leading_zero);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (logger_state->ts_ticks && --logger_state->ts_ticks == 0) {
|
||||
_thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h);
|
||||
_thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h, settings->bit.clock_24h_leading_zero);
|
||||
}
|
||||
break;
|
||||
case EVENT_BACKGROUND_TASK:
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
#include "preferences_face.h"
|
||||
#include "watch.h"
|
||||
|
||||
#define PREFERENCES_FACE_NUM_PREFEFENCES (7)
|
||||
const char preferences_face_titles[PREFERENCES_FACE_NUM_PREFEFENCES][11] = {
|
||||
#define PREFERENCES_FACE_NUM_PREFERENCES (7)
|
||||
const char preferences_face_titles[PREFERENCES_FACE_NUM_PREFERENCES][11] = {
|
||||
"CL ", // Clock: 12 or 24 hour
|
||||
"BT Beep ", // Buttons: should they beep?
|
||||
"TO ", // Timeout: how long before we snap back to the clock face?
|
||||
|
@ -65,7 +65,7 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings
|
|||
movement_move_to_next_face();
|
||||
return false;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
current_page = (current_page + 1) % PREFERENCES_FACE_NUM_PREFEFENCES;
|
||||
current_page = (current_page + 1) % PREFERENCES_FACE_NUM_PREFERENCES;
|
||||
*((uint8_t *)context) = current_page;
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
|
@ -84,6 +84,9 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings
|
|||
break;
|
||||
case 4:
|
||||
settings->bit.led_duration = settings->bit.led_duration + 1;
|
||||
if (settings->bit.led_duration > 3) {
|
||||
settings->bit.led_duration = 0b111;
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
settings->bit.led_green_color = settings->bit.led_green_color + 1;
|
||||
|
@ -93,13 +96,23 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings
|
|||
break;
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
switch (current_page) {
|
||||
case 0:
|
||||
if (settings->bit.clock_mode_24h)
|
||||
settings->bit.clock_24h_leading_zero = !(settings->bit.clock_24h_leading_zero);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
movement_move_to_face(0);
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
|
||||
#ifdef CLOCK_FACE_24H_ONLY
|
||||
if (current_page == 0) current_page++; // Skips past 12/24HR mode
|
||||
#endif
|
||||
watch_display_string((char *)preferences_face_titles[current_page], 0);
|
||||
|
||||
// blink active setting on even-numbered quarter-seconds
|
||||
|
@ -107,8 +120,10 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings
|
|||
char buf[8];
|
||||
switch (current_page) {
|
||||
case 0:
|
||||
if (settings->bit.clock_mode_24h) watch_display_string("24h", 4);
|
||||
else watch_display_string("12h", 4);
|
||||
if (settings->bit.clock_mode_24h) {
|
||||
if (settings->bit.clock_24h_leading_zero) watch_display_string("024h", 4);
|
||||
else watch_display_string("24h", 4);
|
||||
} else watch_display_string("12h", 4);
|
||||
break;
|
||||
case 1:
|
||||
if (settings->bit.button_should_sound) watch_display_string("y", 9);
|
||||
|
@ -159,11 +174,13 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings
|
|||
}
|
||||
break;
|
||||
case 4:
|
||||
if (settings->bit.led_duration) {
|
||||
if (settings->bit.led_duration == 0) {
|
||||
watch_display_string("instnt", 4);
|
||||
} else if (settings->bit.led_duration == 0b111) {
|
||||
watch_display_string("no LEd", 4);
|
||||
} else {
|
||||
sprintf(buf, " %1d SeC", settings->bit.led_duration * 2 - 1);
|
||||
watch_display_string(buf, 4);
|
||||
} else {
|
||||
watch_display_string("no LEd", 4);
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
#include <stdlib.h>
|
||||
#include "set_time_face.h"
|
||||
#include "watch.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
#define SET_TIME_FACE_NUM_SETTINGS (7)
|
||||
const char set_time_face_titles[SET_TIME_FACE_NUM_SETTINGS][3] = {"HR", "M1", "SE", "YR", "MO", "DA", "ZO"};
|
||||
|
@ -33,7 +34,6 @@ static bool _quick_ticks_running;
|
|||
|
||||
static void _handle_alarm_button(movement_settings_t *settings, watch_date_time date_time, uint8_t current_page) {
|
||||
// handles short or long pressing of the alarm button
|
||||
const uint8_t days_in_month[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
||||
|
||||
switch (current_page) {
|
||||
case 0: // hour
|
||||
|
@ -52,14 +52,7 @@ static void _handle_alarm_button(movement_settings_t *settings, watch_date_time
|
|||
date_time.unit.month = (date_time.unit.month % 12) + 1;
|
||||
break;
|
||||
case 5: { // day
|
||||
uint32_t tmp_day = date_time.unit.day; // use a temporary variable to avoid messing up the months
|
||||
tmp_day = tmp_day + 1;
|
||||
// handle February 29th on a leap year
|
||||
if (((tmp_day > days_in_month[date_time.unit.month - 1]) && (date_time.unit.month != 2 || (date_time.unit.year % 4) != 0))
|
||||
|| (date_time.unit.month == 2 && (date_time.unit.year % 4) == 0 && tmp_day > 29)) {
|
||||
tmp_day = 1;
|
||||
}
|
||||
date_time.unit.day = tmp_day;
|
||||
date_time.unit.day = date_time.unit.day + 1;
|
||||
break;
|
||||
}
|
||||
case 6: // time zone
|
||||
|
@ -67,6 +60,8 @@ static void _handle_alarm_button(movement_settings_t *settings, watch_date_time
|
|||
if (settings->bit.time_zone > 40) settings->bit.time_zone = 0;
|
||||
break;
|
||||
}
|
||||
if (date_time.unit.day > days_in_month(date_time.unit.month, date_time.unit.year + WATCH_RTC_REFERENCE_YEAR))
|
||||
date_time.unit.day = 1;
|
||||
watch_rtc_set_date_time(date_time);
|
||||
}
|
||||
|
||||
|
@ -131,10 +126,14 @@ bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, v
|
|||
}
|
||||
|
||||
char buf[11];
|
||||
bool set_leading_zero = false;
|
||||
if (current_page < 3) {
|
||||
watch_set_colon();
|
||||
if (settings->bit.clock_mode_24h) {
|
||||
if (!settings->bit.clock_24h_leading_zero)
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
else if (date_time.unit.hour < 10)
|
||||
set_leading_zero = true;
|
||||
sprintf(buf, "%s %2d%02d%02d", set_time_face_titles[current_page], date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
|
||||
} else {
|
||||
sprintf(buf, "%s %2d%02d%02d", set_time_face_titles[current_page], (date_time.unit.hour % 12) ? (date_time.unit.hour % 12) : 12, date_time.unit.minute, date_time.unit.second);
|
||||
|
@ -175,6 +174,8 @@ bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, v
|
|||
}
|
||||
|
||||
watch_display_string(buf, 0);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include <stdlib.h>
|
||||
#include "set_time_hackwatch_face.h"
|
||||
#include "watch.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
char set_time_hackwatch_face_titles[][3] = {"HR", "M1", "SE", "YR", "MO", "DA", "ZO"};
|
||||
#define set_time_hackwatch_face_NUM_SETTINGS (sizeof(set_time_hackwatch_face_titles) / sizeof(*set_time_hackwatch_face_titles))
|
||||
|
@ -47,7 +48,6 @@ void set_time_hackwatch_face_activate(movement_settings_t *settings, void *conte
|
|||
|
||||
bool set_time_hackwatch_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
uint8_t current_page = *((uint8_t *)context);
|
||||
const uint8_t days_in_month[12] = {31, 28, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31};
|
||||
|
||||
if (event.subsecond == 15) // Delay displayed time update by ~0.5 seconds, to align phase exactly to main clock at 1Hz
|
||||
date_time_settings = watch_rtc_get_date_time();
|
||||
|
@ -119,10 +119,8 @@ bool set_time_hackwatch_face_loop(movement_event_t event, movement_settings_t *s
|
|||
break;
|
||||
case 5: // day
|
||||
date_time_settings.unit.day = date_time_settings.unit.day - 2;
|
||||
// can't set to the 29th on a leap year. if it's february 29, set to 11:59 on the 28th.
|
||||
// and it should roll over.
|
||||
if (date_time_settings.unit.day == 0) {
|
||||
date_time_settings.unit.day = days_in_month[date_time_settings.unit.month - 1];
|
||||
date_time_settings.unit.day = days_in_month(date_time_settings.unit.month, date_time_settings.unit.year + WATCH_RTC_REFERENCE_YEAR);
|
||||
} else
|
||||
date_time_settings.unit.day++;
|
||||
break;
|
||||
|
@ -167,17 +165,14 @@ bool set_time_hackwatch_face_loop(movement_event_t event, movement_settings_t *s
|
|||
break;
|
||||
case 5: // day
|
||||
date_time_settings.unit.day = date_time_settings.unit.day + 1;
|
||||
// can't set to the 29th on a leap year. if it's february 29, set to 11:59 on the 28th.
|
||||
// and it should roll over.
|
||||
if (date_time_settings.unit.day > days_in_month[date_time_settings.unit.month - 1]) {
|
||||
date_time_settings.unit.day = 1;
|
||||
}
|
||||
break;
|
||||
case 6: // time zone
|
||||
settings->bit.time_zone++;
|
||||
if (settings->bit.time_zone > 40) settings->bit.time_zone = 0;
|
||||
break;
|
||||
}
|
||||
if (date_time_settings.unit.day > days_in_month(date_time_settings.unit.month, date_time_settings.unit.year + WATCH_RTC_REFERENCE_YEAR))
|
||||
date_time_settings.unit.day = 1;
|
||||
if (current_page != 2) // Do not set time when we are at seconds, it was already set previously
|
||||
watch_rtc_set_date_time(date_time_settings);
|
||||
//TODO: Do not update whole RTC, just what we are changing
|
||||
|
@ -194,10 +189,14 @@ bool set_time_hackwatch_face_loop(movement_event_t event, movement_settings_t *s
|
|||
}
|
||||
|
||||
char buf[11];
|
||||
bool set_leading_zero = false;
|
||||
if (current_page < 3) {
|
||||
watch_set_colon();
|
||||
if (settings->bit.clock_mode_24h) {
|
||||
if (!settings->bit.clock_24h_leading_zero)
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
else if (date_time_settings.unit.hour < 10)
|
||||
set_leading_zero = true;
|
||||
sprintf(buf,
|
||||
"%s %2d%02d%02d",
|
||||
set_time_hackwatch_face_titles[current_page],
|
||||
|
@ -263,6 +262,8 @@ bool set_time_hackwatch_face_loop(movement_event_t event, movement_settings_t *s
|
|||
}
|
||||
|
||||
watch_display_string(buf, 0);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
1385
utils/wordle_face/wordle_list.py
Normal file
1385
utils/wordle_face/wordle_list.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -77,6 +77,7 @@ void watch_register_extwake_callback(uint8_t pin, ext_irq_cb_t callback, bool le
|
|||
RTC->MODE2.TAMPCTRL.reg = config;
|
||||
// re-enable the RTC
|
||||
RTC->MODE2.CTRLA.bit.ENABLE = 1;
|
||||
while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be enabled
|
||||
|
||||
NVIC_ClearPendingIRQ(RTC_IRQn);
|
||||
NVIC_EnableIRQ(RTC_IRQn);
|
||||
|
|
|
@ -45,7 +45,15 @@ void thermistor_driver_disable(void) {
|
|||
// Disable the enable pin's output circuitry.
|
||||
watch_disable_digital_output(THERMISTOR_ENABLE_PIN);
|
||||
}
|
||||
|
||||
#if __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
float thermistor_driver_get_temperature(void)
|
||||
{
|
||||
return EM_ASM_DOUBLE({
|
||||
return temp_c || 25.0;
|
||||
});
|
||||
}
|
||||
#else
|
||||
float thermistor_driver_get_temperature(void) {
|
||||
// set the enable pin to the level that powers the thermistor circuit.
|
||||
watch_set_pin_level(THERMISTOR_ENABLE_PIN, THERMISTOR_ENABLE_VALUE);
|
||||
|
@ -56,3 +64,4 @@ float thermistor_driver_get_temperature(void) {
|
|||
|
||||
return watch_utility_thermistor_temperature(value, THERMISTOR_HIGH_SIDE, THERMISTOR_B_COEFFICIENT, THERMISTOR_NOMINAL_TEMPERATURE, THERMISTOR_NOMINAL_RESISTANCE, THERMISTOR_SERIES_RESISTANCE);
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -43,6 +43,8 @@ void watch_display_character(uint8_t character, uint8_t position) {
|
|||
else if (character == 'M' || character == 'm' || character == 'N') character = 'n'; // M and uppercase N need to be lowercase n
|
||||
else if (character == 'c') character = 'C'; // C needs to be uppercase
|
||||
else if (character == 'J') character = 'j'; // same
|
||||
else if (character == 't' || character == 'T') character = '+'; // t in those locations looks like E otherwise
|
||||
else if (character == 'y' || character == 'Y') character = '4'; // y in those locations looks like g otherwise
|
||||
else if (character == 'v' || character == 'V' || character == 'U' || character == 'W' || character == 'w') character = 'u'; // bottom segment duplicated, so show in top half
|
||||
} else {
|
||||
if (character == 'u') character = 'v'; // we can use the bottom segment; move to lower half
|
||||
|
|
|
@ -315,3 +315,11 @@ uint32_t watch_utility_offset_timestamp(uint32_t now, int8_t hours, int8_t minut
|
|||
new += seconds;
|
||||
return new;
|
||||
}
|
||||
|
||||
uint8_t days_in_month(uint8_t month, uint16_t year) {
|
||||
static const uint8_t days_in_month[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
||||
uint8_t days = days_in_month[month - 1];
|
||||
if (month == 2 && is_leap(year))
|
||||
days += 1;
|
||||
return days;
|
||||
}
|
||||
|
|
|
@ -164,4 +164,10 @@ float watch_utility_thermistor_temperature(uint16_t value, bool highside, float
|
|||
*/
|
||||
uint32_t watch_utility_offset_timestamp(uint32_t now, int8_t hours, int8_t minutes, int8_t seconds);
|
||||
|
||||
/** @brief Returns the number of days in a month. It also handles Leap Years for February.
|
||||
* @param month The month of the date (1-12)
|
||||
* @param year The year of the date (ex. 2022)
|
||||
*/
|
||||
uint8_t days_in_month(uint8_t month, uint16_t year);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -89,6 +89,7 @@ void main_loop_sleep(uint32_t ms) {
|
|||
main_loop_set_sleeping(true);
|
||||
emscripten_sleep(ms);
|
||||
main_loop_set_sleeping(false);
|
||||
animation_frame_id = ANIMATION_FRAME_ID_INVALID;
|
||||
}
|
||||
|
||||
bool main_loop_is_sleeping(void) {
|
||||
|
|
|
@ -905,6 +905,11 @@
|
|||
<div>
|
||||
<button onclick="getLocation()">Set register (will prompt for access)</button>
|
||||
</div>
|
||||
<h2>Temp.</h2>
|
||||
<div>
|
||||
<input type="number" min="-100" max="120" id="temp-c" />C
|
||||
<button onclick="setTemp()">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit="sendText(); return false" style="display: flex; flex-direction: column; width: 100%">
|
||||
|
@ -962,6 +967,7 @@
|
|||
lat = 0;
|
||||
lon = 0;
|
||||
tx = "";
|
||||
temp_c = 25.0;
|
||||
function updateLocation(location) {
|
||||
lat = Math.round(location.coords.latitude * 100);
|
||||
lon = Math.round(location.coords.longitude * 100);
|
||||
|
@ -1038,10 +1044,25 @@
|
|||
document.getElementById(skin).checked = true;
|
||||
setSkin(skin);
|
||||
}
|
||||
|
||||
function setTemp() {
|
||||
let tempInput = document.getElementById("temp-c");
|
||||
if (!tempInput) {
|
||||
return console.warn("no input found");
|
||||
}
|
||||
if (tempInput.value == "") {
|
||||
return console.warn("no value in input");
|
||||
}
|
||||
|
||||
try {
|
||||
temp_c = Number.parseFloat(tempInput.value);
|
||||
} catch (e) {
|
||||
return console.warn("input value is not a valid float:", tempInput.value, e);
|
||||
}
|
||||
}
|
||||
loadPrefs();
|
||||
</script>
|
||||
{{{ SCRIPT }}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
Loading…
Reference in a new issue