Merge branch 'next' - new faces before the freeze

Merge the next branch containing numerous new sensor watch faces
as well as some new features. Not all of them made it in the end
and we even had to revert improvements merged in last next due to
issues that weren't found during testing. Still, I am very proud
to be merging in and closing over 20 pull requests.

I believe the project is in the best possible shape it can be
before the movement 2.0 refactor.

Reviewed-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>
Tested-on-hardware-by: David Volovskiy <devolov@gmail.com>
Tested-on-hardware-by: CarpeNoctem <cryptomax@pm.me>
GitHub-Pull-Request: https://github.com/joeycastillo/Sensor-Watch/pull/469
This commit is contained in:
Matheus Afonso Martins Moreira 2024-09-17 17:36:40 -03:00
commit bf4d461d8c
56 changed files with 7843 additions and 145 deletions

14
make.mk
View file

@ -215,6 +215,20 @@ SRCS += \
endif 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) ifeq ($(COLOR), BLUE)
CFLAGS += -DWATCH_IS_BLUE_BOARD CFLAGS += -DWATCH_IS_BLUE_BOARD
endif endif

View file

@ -52,6 +52,7 @@ SRCS += \
../shell.c \ ../shell.c \
../shell_cmd_list.c \ ../shell_cmd_list.c \
../watch_faces/clock/simple_clock_face.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/clock_face.c \
../watch_faces/clock/world_clock_face.c \ ../watch_faces/clock/world_clock_face.c \
../watch_faces/clock/beats_face.c \ ../watch_faces/clock/beats_face.c \
@ -129,6 +130,17 @@ SRCS += \
../watch_faces/clock/minute_repeater_decimal_face.c \ ../watch_faces/clock/minute_repeater_decimal_face.c \
../watch_faces/complication/tuning_tones_face.c \ ../watch_faces/complication/tuning_tones_face.c \
../watch_faces/complication/kitchen_conversions_face.c \ ../watch_faces/complication/kitchen_conversions_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 \
# New watch faces go above this line. # 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. # Leave this line at the bottom of the file; it has all the targets for making your project.

View file

@ -95,31 +95,6 @@
#define MOVEMENT_DEFAULT_LED_DURATION 1 #define MOVEMENT_DEFAULT_LED_DURATION 1
#endif #endif
// Default to no set location latitude
#ifndef MOVEMENT_DEFAULT_LATITUDE
#define MOVEMENT_DEFAULT_LATITUDE 0
#endif
// Default to no set location longitude
#ifndef MOVEMENT_DEFAULT_LONGITUDE
#define MOVEMENT_DEFAULT_LONGITUDE 0
#endif
// Default to no set birthdate year
#ifndef MOVEMENT_DEFAULT_BIRTHDATE_YEAR
#define MOVEMENT_DEFAULT_BIRTHDATE_YEAR 0
#endif
// Default to no set birthdate month
#ifndef MOVEMENT_DEFAULT_BIRTHDATE_MONTH
#define MOVEMENT_DEFAULT_BIRTHDATE_MONTH 0
#endif
// Default to no set birthdate day
#ifndef MOVEMENT_DEFAULT_BIRTHDATE_DAY
#define MOVEMENT_DEFAULT_BIRTHDATE_DAY 0
#endif
#if __EMSCRIPTEN__ #if __EMSCRIPTEN__
#include <emscripten.h> #include <emscripten.h>
#endif #endif
@ -264,14 +239,24 @@ void movement_request_tick_frequency(uint8_t freq) {
} }
void movement_illuminate_led(void) { 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, 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); movement_state.settings.bit.led_green_color ? (0xF | movement_state.settings.bit.led_green_color << 4) : 0);
movement_state.light_ticks = (movement_state.settings.bit.led_duration * 2 - 1) * 128; 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(); _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) { bool movement_default_loop_handler(movement_event_t event, movement_settings_t *settings) {
(void)settings; (void)settings;
@ -282,6 +267,11 @@ bool movement_default_loop_handler(movement_event_t event, movement_settings_t *
case EVENT_LIGHT_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_DOWN:
movement_illuminate_led(); movement_illuminate_led();
break; break;
case EVENT_LIGHT_BUTTON_UP:
if (movement_state.settings.bit.led_duration == 0) {
_movement_led_off();
}
break;
case EVENT_MODE_LONG_PRESS: case EVENT_MODE_LONG_PRESS:
if (MOVEMENT_SECONDARY_FACE_INDEX && movement_state.current_face_idx == 0) { if (MOVEMENT_SECONDARY_FACE_INDEX && movement_state.current_face_idx == 0) {
movement_move_to_face(MOVEMENT_SECONDARY_FACE_INDEX); movement_move_to_face(MOVEMENT_SECONDARY_FACE_INDEX);
@ -416,11 +406,7 @@ void app_init(void) {
movement_state.settings.bit.to_interval = MOVEMENT_DEFAULT_TIMEOUT_INTERVAL; 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.le_interval = MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL;
movement_state.settings.bit.led_duration = MOVEMENT_DEFAULT_LED_DURATION; movement_state.settings.bit.led_duration = MOVEMENT_DEFAULT_LED_DURATION;
movement_state.location.bit.latitude = MOVEMENT_DEFAULT_LATITUDE;
movement_state.location.bit.longitude = MOVEMENT_DEFAULT_LONGITUDE;
movement_state.birthdate.bit.year = MOVEMENT_DEFAULT_BIRTHDATE_YEAR;
movement_state.birthdate.bit.month = MOVEMENT_DEFAULT_BIRTHDATE_MONTH;
movement_state.birthdate.bit.day = MOVEMENT_DEFAULT_BIRTHDATE_DAY;
movement_state.light_ticks = -1; movement_state.light_ticks = -1;
movement_state.alarm_ticks = -1; movement_state.alarm_ticks = -1;
movement_state.next_available_backup_register = 4; movement_state.next_available_backup_register = 4;
@ -443,14 +429,10 @@ void app_init(void) {
void app_wake_from_backup(void) { void app_wake_from_backup(void) {
movement_state.settings.reg = watch_get_backup_data(0); movement_state.settings.reg = watch_get_backup_data(0);
movement_state.location.reg = watch_get_backup_data(1);
movement_state.birthdate.reg = watch_get_backup_data(2);
} }
void app_setup(void) { void app_setup(void) {
watch_store_backup_data(movement_state.settings.reg, 0); watch_store_backup_data(movement_state.settings.reg, 0);
watch_store_backup_data(movement_state.location.reg, 1);
watch_store_backup_data(movement_state.birthdate.reg, 2);
static bool is_first_launch = true; static bool is_first_launch = true;
@ -544,9 +526,7 @@ bool app_loop(void) {
if (watch_get_pin_level(BTN_LIGHT)) { if (watch_get_pin_level(BTN_LIGHT)) {
movement_state.light_ticks = 1; movement_state.light_ticks = 1;
} else { } else {
watch_set_led_off(); _movement_led_off();
movement_state.light_ticks = -1;
_movement_disable_fast_tick_if_possible();
} }
} }
@ -584,6 +564,17 @@ bool app_loop(void) {
event.subsecond = movement_state.subsecond; event.subsecond = movement_state.subsecond;
// the first trip through the loop overrides the can_sleep state // 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]); 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; event.event_type = EVENT_NONE;
} }

View file

@ -50,7 +50,7 @@ typedef union {
uint8_t to_interval : 2; // an inactivity interval for asking the active face to resign. 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). 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 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_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 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. 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 // 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. // 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_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 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. 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; } bit;
uint32_t reg; uint32_t reg;
} movement_settings_t; } movement_settings_t;
@ -242,8 +243,6 @@ typedef struct {
typedef struct { typedef struct {
// properties stored in BACKUP register // properties stored in BACKUP register
movement_settings_t settings; movement_settings_t settings;
movement_location_t location;
movement_birthdate_t birthdate;
// transient properties // transient properties
int16_t current_face_idx; int16_t current_face_idx;

View file

@ -76,15 +76,15 @@ const watch_face_t watch_faces[] = {
/* Set the timeout before switching to low energy mode /* Set the timeout before switching to low energy mode
* Valid values are: * Valid values are:
* 0: Never * 0: Never
* 1: 10 mins * 1: 1 hour
* 2: 1 hour * 2: 2 hours
* 3: 2 hours * 3: 6 hours
* 4: 6 hours * 4: 12 hours
* 5: 12 hours * 5: 1 day
* 6: 1 day * 6: 2 days
* 7: 7 days * 7: 7 days
*/ */
#define MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL 2 #define MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL 1
/* Set the led duration /* Set the led duration
* Valid values are: * Valid values are:
@ -95,20 +95,4 @@ const watch_face_t watch_faces[] = {
*/ */
#define MOVEMENT_DEFAULT_LED_DURATION 1 #define MOVEMENT_DEFAULT_LED_DURATION 1
/* The latitude and longitude used for the wearers location
* Set signed values in 1/100ths of a degree
*/
#define MOVEMENT_DEFAULT_LATITUDE 0
#define MOVEMENT_DEFAULT_LONGITUDE 0
/* The wearers birthdate
* Valid values:
* Year: 1 - 4095
* Month: 1 - 12
* Day: 1 - 31
*/
#define MOVEMENT_DEFAULT_BIRTHDATE_YEAR 0
#define MOVEMENT_DEFAULT_BIRTHDATE_MONTH 0
#define MOVEMENT_DEFAULT_BIRTHDATE_DAY 0
#endif // MOVEMENT_CONFIG_H_ #endif // MOVEMENT_CONFIG_H_

View file

@ -67,6 +67,33 @@ int8_t signal_tune[] = {
}; };
#endif // SIGNAL_TUNE_MARIO_THEME #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 #ifdef SIGNAL_TUNE_KIM_POSSIBLE
int8_t signal_tune[] = { int8_t signal_tune[] = {
BUZZER_NOTE_G7, 6, BUZZER_NOTE_G7, 6,
@ -119,4 +146,60 @@ int8_t signal_tune[] = {
}; };
#endif // SIGNAL_TUNE_LAYLA #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_ #endif // MOVEMENT_CUSTOM_SIGNAL_TUNES_H_

View file

@ -26,6 +26,7 @@
#define MOVEMENT_FACES_H_ #define MOVEMENT_FACES_H_
#include "simple_clock_face.h" #include "simple_clock_face.h"
#include "close_enough_clock_face.h"
#include "clock_face.h" #include "clock_face.h"
#include "world_clock_face.h" #include "world_clock_face.h"
#include "preferences_face.h" #include "preferences_face.h"
@ -104,6 +105,17 @@
#include "minute_repeater_decimal_face.h" #include "minute_repeater_decimal_face.h"
#include "tuning_tones_face.h" #include "tuning_tones_face.h"
#include "kitchen_conversions_face.h" #include "kitchen_conversions_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"
// New includes go above this line. // New includes go above this line.
#endif // MOVEMENT_FACES_H_ #endif // MOVEMENT_FACES_H_

View file

@ -60,6 +60,10 @@ static bool clock_is_in_24h_mode(movement_settings_t *settings) {
#endif #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) { static void clock_indicate(WatchIndicatorSegment indicator, bool on) {
if (on) { if (on) {
watch_set_indicator(indicator); watch_set_indicator(indicator);
@ -124,13 +128,13 @@ static void clock_toggle_time_signal(clock_state_t *clock) {
clock_indicate_time_signal(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]; char buf[10 + 1];
snprintf( snprintf(
buf, buf,
sizeof(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), watch_utility_get_weekday(date_time),
date_time.unit.day, date_time.unit.day,
date_time.unit.hour, date_time.unit.hour,
@ -180,7 +184,7 @@ static void clock_display_clock(movement_settings_t *settings, clock_state_t *cl
clock_indicate_pm(settings, current); clock_indicate_pm(settings, current);
current = clock_24h_to_12h(current); current = clock_24h_to_12h(current);
} }
clock_display_all(current); clock_display_all(current, clock_should_set_leading_zero(settings));
} }
} }

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

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

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

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

View 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.
}

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

View file

@ -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 (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 // handle chime indicator
if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL); 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. // ...and set the LAP indicator if low.
if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP); 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) { 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. // everything before seconds is the same, don't waste cycles setting those segments.
watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8); watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8);
@ -132,6 +133,8 @@ bool repetition_minute_face_loop(movement_event_t event, movement_settings_t *se
} }
date_time.unit.hour %= 12; date_time.unit.hour %= 12;
if (date_time.unit.hour == 0) 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; pos = 0;
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { 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); watch_display_string(buf, pos);
if (set_leading_zero)
watch_display_string("0", 4);
// handle alarm indicator // handle alarm indicator
if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state); if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state);
break; break;

View file

@ -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 (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 // handle chime indicator
if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL); 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. // ...and set the LAP indicator if low.
if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP); 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) { 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. // everything before seconds is the same, don't waste cycles setting those segments.
watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8); watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8);
@ -158,6 +159,8 @@ bool simple_clock_bin_led_face_loop(movement_event_t event, movement_settings_t
} }
date_time.unit.hour %= 12; date_time.unit.hour %= 12;
if (date_time.unit.hour == 0) 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; pos = 0;
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { 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); watch_display_string(buf, pos);
if (set_leading_zero)
watch_display_string("0", 4);
// handle alarm indicator // handle alarm indicator
if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state); if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state);
} }

View file

@ -99,6 +99,7 @@ bool simple_clock_face_loop(movement_event_t event, movement_settings_t *setting
// ...and set the LAP indicator if low. // ...and set the LAP indicator if low.
if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP); 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) { 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. // everything before seconds is the same, don't waste cycles setting those segments.
watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8); watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8);
@ -122,6 +123,11 @@ bool simple_clock_face_loop(movement_event_t event, movement_settings_t *setting
if (date_time.unit.hour == 0) date_time.unit.hour = 12; if (date_time.unit.hour == 0) date_time.unit.hour = 12;
} }
#endif #endif
if (settings->bit.clock_mode_24h && settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) {
set_leading_zero = true;
}
pos = 0; pos = 0;
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
if (!watch_tick_animation_is_running()) watch_start_tick_animation(500); if (!watch_tick_animation_is_running()) watch_start_tick_animation(500);
@ -131,6 +137,10 @@ bool simple_clock_face_loop(movement_event_t event, movement_settings_t *setting
} }
} }
watch_display_string(buf, pos); watch_display_string(buf, pos);
if (set_leading_zero)
watch_display_string("0", 4);
// handle alarm indicator // handle alarm indicator
if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state); if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state);
break; break;

View file

@ -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 (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 // handle chime indicator
if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL); 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. // ...and set the LAP indicator if low.
if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP); 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) { if ((date_time.reg >> 12) == (previous_date_time >> 12) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
// everything before minutes is the same. // everything before minutes is the same.
pos = 6; pos = 6;
@ -109,6 +110,8 @@ bool weeknumber_clock_face_loop(movement_event_t event, movement_settings_t *set
} }
date_time.unit.hour %= 12; date_time.unit.hour %= 12;
if (date_time.unit.hour == 0) 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; pos = 0;
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { 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); watch_display_string(buf, pos);
if (set_leading_zero)
watch_display_string("0", 4);
// handle alarm indicator // handle alarm indicator
if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state); if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state);
break; break;

View file

@ -174,7 +174,7 @@ static bool mode_display(movement_event_t event, movement_settings_t *settings,
if (refresh_face) { if (refresh_face) {
watch_clear_indicator(WATCH_INDICATOR_SIGNAL); watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
watch_set_colon(); 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); watch_set_indicator(WATCH_INDICATOR_24H);
state->previous_date_time = REFRESH_TIME; 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; previous_date_time = state->previous_date_time;
state->previous_date_time = date_time.reg; 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) { 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. */ /* Everything before seconds is the same, don't waste cycles setting those segments. */
pos = 8; pos = 8;
@ -208,7 +209,9 @@ static bool mode_display(movement_event_t event, movement_settings_t *settings,
date_time.unit.hour %= 12; date_time.unit.hour %= 12;
if (date_time.unit.hour == 0) if (date_time.unit.hour == 0)
date_time.unit.hour = 12; date_time.unit.hour = 12;
} } else if (settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) {
set_leading_zero = true;
}
pos = 0; pos = 0;
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
@ -230,6 +233,8 @@ static bool mode_display(movement_event_t event, movement_settings_t *settings,
} }
} }
watch_display_string(buf, pos); watch_display_string(buf, pos);
if (set_leading_zero)
watch_display_string("0", 4);
break; break;
case EVENT_ALARM_BUTTON_UP: case EVENT_ALARM_BUTTON_UP:
state->current_zone = find_selected_zone(state, FORWARD); state->current_zone = find_selected_zone(state, FORWARD);

View file

@ -60,7 +60,7 @@ static bool world_clock_face_do_display_mode(movement_event_t event, movement_se
watch_date_time date_time; watch_date_time date_time;
switch (event.event_type) { switch (event.event_type) {
case EVENT_ACTIVATE: 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(); watch_set_colon();
state->previous_date_time = 0xFFFFFFFF; state->previous_date_time = 0xFFFFFFFF;
// fall through // 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; previous_date_time = state->previous_date_time;
state->previous_date_time = date_time.reg; 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) { 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. // everything before seconds is the same, don't waste cycles setting those segments.
pos = 8; 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; date_time.unit.hour %= 12;
if (date_time.unit.hour == 0) 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; pos = 0;
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { 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); watch_display_string(buf, pos);
if (set_leading_zero)
watch_display_string("0", 4);
break; break;
case EVENT_ALARM_LONG_PRESS: case EVENT_ALARM_LONG_PRESS:
movement_request_tick_frequency(4); movement_request_tick_frequency(4);

View file

@ -293,6 +293,7 @@ static void _activity_update_logging_screen(movement_settings_t *settings, activ
} }
// Briefly, show time without seconds // Briefly, show time without seconds
else { else {
bool set_leading_zero = false;
watch_clear_indicator(WATCH_INDICATOR_LAP); watch_clear_indicator(WATCH_INDICATOR_LAP);
watch_date_time now = watch_rtc_get_date_time(); watch_date_time now = watch_rtc_get_date_time();
uint8_t hour = now.unit.hour; 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); watch_set_indicator(WATCH_INDICATOR_PM);
hour %= 12; hour %= 12;
if (hour == 0) hour = 12; if (hour == 0) hour = 12;
} } else {
else {
watch_set_indicator(WATCH_INDICATOR_24H);
watch_clear_indicator(WATCH_INDICATOR_PM); 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); sprintf(activity_buf, "%2d%02d ", hour, now.unit.minute);
watch_set_colon(); watch_set_colon();
watch_display_string(activity_buf, 4); watch_display_string(activity_buf, 4);
if (set_leading_zero)
watch_display_string("0", 4);
} }
} }

View file

@ -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; i = state->alarm[state->alarm_idx].day + 1;
} }
//handle am/pm for hour display //handle am/pm for hour display
bool set_leading_zero = false;
uint8_t h = state->alarm[state->alarm_idx].hour; uint8_t h = state->alarm[state->alarm_idx].hour;
if (!settings->bit.clock_mode_24h) { if (!settings->bit.clock_mode_24h) {
if (h >= 12) { 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); watch_clear_indicator(WATCH_INDICATOR_PM);
} }
if (h == 0) h = 12; 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], _dow_strings[i][0], _dow_strings[i][1],
(state->alarm_idx + 1), (state->alarm_idx + 1),
h, h,

View file

@ -1,6 +1,7 @@
/* /*
* MIT License * MIT License
* *
* Copyright (c) 2024 Joseph Bryant
* Copyright (c) 2023 Konrad Rieck * Copyright (c) 2023 Konrad Rieck
* Copyright (c) 2022 Wesley Ellis * 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); watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
} }
static void start(countdown_state_t *state, movement_settings_t *settings) { static void schedule_countdown(countdown_state_t *state, movement_settings_t *settings) {
watch_date_time now = watch_rtc_get_date_time();
state->mode = cd_running; // Calculate the new state->now_ts but don't update it until we've updated the target -
state->now_ts = watch_utility_date_time_to_unix_time(now, get_tz_offset(settings)); // avoid possible race where the old target is compared to the new time and immediately triggers
state->target_ts = watch_utility_offset_timestamp(state->now_ts, state->hours, state->minutes, state->seconds); 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)); 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); movement_schedule_background_task_for_face(state->watch_face_index, target_dt);
watch_set_indicator(WATCH_INDICATOR_BELL);
} }
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) { static void draw(countdown_state_t *state, uint8_t subsecond) {
char buf[16]; char buf[16];
@ -100,7 +114,7 @@ static void draw(countdown_state_t *state, uint8_t subsecond) {
break; break;
case cd_reset: case cd_reset:
case cd_paused: case cd_paused:
watch_clear_indicator(WATCH_INDICATOR_BELL); watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
sprintf(buf, "CD %2d%02d%02d", state->hours, state->minutes, state->seconds); sprintf(buf, "CD %2d%02d%02d", state->hours, state->minutes, state->seconds);
break; break;
case cd_setting: case cd_setting:
@ -127,13 +141,13 @@ static void draw(countdown_state_t *state, uint8_t subsecond) {
static void pause(countdown_state_t *state) { static void pause(countdown_state_t *state) {
state->mode = cd_paused; state->mode = cd_paused;
movement_cancel_background_task(); movement_cancel_background_task_for_face(state->watch_face_index);
watch_clear_indicator(WATCH_INDICATOR_BELL); watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
} }
static void reset(countdown_state_t *state) { static void reset(countdown_state_t *state) {
state->mode = cd_reset; state->mode = cd_reset;
movement_cancel_background_task(); movement_cancel_background_task_for_face(state->watch_face_index);
load_countdown(state); load_countdown(state);
} }
@ -142,6 +156,15 @@ static void ring(countdown_state_t *state) {
reset(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) { static void settings_increment(countdown_state_t *state) {
switch(state->selection) { switch(state->selection) {
case 0: case 0:
@ -170,6 +193,7 @@ void countdown_face_setup(movement_settings_t *settings, uint8_t watch_face_inde
memset(*context_ptr, 0, sizeof(countdown_state_t)); memset(*context_ptr, 0, sizeof(countdown_state_t));
state->minutes = DEFAULT_MINUTES; state->minutes = DEFAULT_MINUTES;
state->mode = cd_reset; state->mode = cd_reset;
state->watch_face_index = watch_face_index;
store_countdown(state); store_countdown(state);
} }
} }
@ -180,9 +204,11 @@ void countdown_face_activate(movement_settings_t *settings, void *context) {
if(state->mode == cd_running) { if(state->mode == cd_running) {
watch_date_time now = watch_rtc_get_date_time(); watch_date_time now = watch_rtc_get_date_time();
state->now_ts = watch_utility_date_time_to_unix_time(now, get_tz_offset(settings)); 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(); watch_set_colon();
if(state->repeat)
watch_set_indicator(WATCH_INDICATOR_BELL);
movement_request_tick_frequency(1); movement_request_tick_frequency(1);
quick_ticks_running = false; quick_ticks_running = false;
@ -252,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. // Only start the timer if we have a valid time.
start(state, settings); start(state, settings);
button_beep(settings); button_beep(settings);
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
} }
break; break;
case cd_setting: case cd_setting:
@ -261,9 +288,19 @@ bool countdown_face_loop(movement_event_t event, movement_settings_t *settings,
draw(state, event.subsecond); draw(state, event.subsecond);
break; break;
case EVENT_ALARM_LONG_PRESS: case EVENT_ALARM_LONG_PRESS:
if (state->mode == cd_setting) { switch(state->mode) {
quick_ticks_running = true; case cd_setting:
movement_request_tick_frequency(8); 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; break;
case EVENT_LIGHT_LONG_PRESS: case EVENT_LIGHT_LONG_PRESS:
@ -285,7 +322,7 @@ bool countdown_face_loop(movement_event_t event, movement_settings_t *settings,
abort_quick_ticks(state); abort_quick_ticks(state);
break; break;
case EVENT_BACKGROUND_TASK: case EVENT_BACKGROUND_TASK:
ring(state); times_up(settings, state);
break; break;
case EVENT_TIMEOUT: case EVENT_TIMEOUT:
abort_quick_ticks(state); abort_quick_ticks(state);

View file

@ -62,6 +62,8 @@ typedef struct {
uint8_t set_seconds; uint8_t set_seconds;
uint8_t selection; uint8_t selection;
countdown_mode_t mode; countdown_mode_t mode;
bool repeat;
uint8_t watch_face_index;
} countdown_state_t; } countdown_state_t;

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

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

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

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

View 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.
}

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

View 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.
}

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

View file

@ -228,6 +228,7 @@ static void _planetary_hours(movement_settings_t *settings, planetary_hours_stat
uint8_t weekday, planet, planetary_hour; uint8_t weekday, planet, planetary_hour;
uint32_t current_hour_epoch; uint32_t current_hour_epoch;
watch_date_time scratch_time; watch_date_time scratch_time;
bool set_leading_zero = false;
// check if we have a location. If not, display error // check if we have a location. If not, display error
if ( state->no_location ) { if ( state->no_location ) {
@ -253,7 +254,7 @@ static void _planetary_hours(movement_settings_t *settings, planetary_hours_stat
return; 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 // roll over hour iterator
if ( state->hour < 0 ) state->hour = 23; 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; scratch_time.unit.hour %= 12;
if (scratch_time.unit.hour == 0) 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 // planetary ruler of the hour
@ -328,6 +331,8 @@ static void _planetary_hours(movement_settings_t *settings, planetary_hours_stat
watch_set_colon(); watch_set_colon();
watch_display_string(buf, 0); watch_display_string(buf, 0);
if (set_leading_zero)
watch_display_string("0", 4);
if ( state->ruler == 2 ) _planetary_icon(planet); if ( state->ruler == 2 ) _planetary_icon(planet);
} }

View file

@ -206,6 +206,7 @@ static void _planetary_time(movement_event_t event, movement_settings_t *setting
double night_hour_count = 0.0; double night_hour_count = 0.0;
uint8_t weekday, planet, planetary_hour; uint8_t weekday, planet, planetary_hour;
double hour_duration, current_hour, current_minute, current_second; double hour_duration, current_hour, current_minute, current_second;
bool set_leading_zero = false;
watch_set_colon(); watch_set_colon();
@ -218,7 +219,7 @@ static void _planetary_time(movement_event_t event, movement_settings_t *setting
return; 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 // PM for night hours, otherwise the night hours are counted from 13
if ( state->night ) { 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.minute = floor(current_minute);
state->scratch.unit.second = (uint8_t)floor(current_second) % 60; 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) // 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; 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); 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); watch_display_string(buf, 0);
if (set_leading_zero)
watch_display_string("0", 4);
if ( state->ruler == 2 ) _planetary_icon(planet); if ( state->ruler == 2 ) _planetary_icon(planet);

View 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();
}

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

View 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);
}

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

View file

@ -93,7 +93,7 @@ static void _sunrise_sunset_face_update(movement_settings_t *settings, sunrise_s
} }
watch_set_colon(); 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; rise += hours_from_utc;
set += 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 (date_time.reg < scratch_time.reg || show_next_match) {
if (state->rise_index == 0 || show_next_match) { if (state->rise_index == 0 || show_next_match) {
bool set_leading_zero = false;
if (!settings->bit.clock_mode_24h) { if (!settings->bit.clock_mode_24h) {
if (watch_utility_convert_to_12_hour(&scratch_time)) watch_set_indicator(WATCH_INDICATOR_PM); if (watch_utility_convert_to_12_hour(&scratch_time)) watch_set_indicator(WATCH_INDICATOR_PM);
else watch_clear_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); 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); watch_display_string(buf, 0);
if (set_leading_zero)
watch_display_string("0", 4);
return; return;
} else { } else {
show_next_match = true; 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 (date_time.reg < scratch_time.reg || show_next_match) {
if (state->rise_index == 0 || show_next_match) { if (state->rise_index == 0 || show_next_match) {
bool set_leading_zero = false;
if (!settings->bit.clock_mode_24h) { if (!settings->bit.clock_mode_24h) {
if (watch_utility_convert_to_12_hour(&scratch_time)) watch_set_indicator(WATCH_INDICATOR_PM); if (watch_utility_convert_to_12_hour(&scratch_time)) watch_set_indicator(WATCH_INDICATOR_PM);
else watch_clear_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); 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); watch_display_string(buf, 0);
if (set_leading_zero)
watch_display_string("0", 4);
return; return;
} else { } else {
show_next_match = true; show_next_match = true;

View file

@ -27,47 +27,162 @@
#include "tally_face.h" #include "tally_face.h"
#include "watch.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 tally_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
(void) settings; (void) settings;
(void) watch_face_index; (void) watch_face_index;
if (*context_ptr == NULL) { if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(tally_state_t)); *context_ptr = malloc(sizeof(tally_state_t));
memset(*context_ptr, 0, 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 tally_face_activate(movement_settings_t *settings, void *context) {
(void) settings; (void) settings;
(void) context; (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) { bool tally_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
(void) settings;
tally_state_t *state = (tally_state_t *)context; 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) { switch (event.event_type) {
case EVENT_ALARM_BUTTON_UP: case EVENT_TICK:
// increment tally index if (_quick_ticks_running) {
state->tally_idx++; bool light_pressed = watch_get_pin_level(BTN_LIGHT);
if (state->tally_idx > 999999) { //0-999,999 bool alarm_pressed = watch_get_pin_level(BTN_ALARM);
//reset tally index and play a reset tune if (light_pressed && alarm_pressed) stop_quick_cyc();
state->tally_idx = 0; else if (light_pressed) tally_face_increment(state, settings->bit.button_should_sound);
watch_buzzer_play_note(BUZZER_NOTE_G6, 30); else if (alarm_pressed) tally_face_decrement(state, settings->bit.button_should_sound);
watch_buzzer_play_note(BUZZER_NOTE_REST, 30); else stop_quick_cyc();
} }
print_tally(state); break;
watch_buzzer_play_note(BUZZER_NOTE_E6, 30); case EVENT_ALARM_BUTTON_UP:
tally_face_decrement(state, settings->bit.button_should_sound);
break; break;
case EVENT_ALARM_LONG_PRESS: case EVENT_ALARM_LONG_PRESS:
state->tally_idx = 0; // reset tally index tally_face_decrement(state, settings->bit.button_should_sound);
//play a reset tune start_quick_cyc();
watch_buzzer_play_note(BUZZER_NOTE_G6, 30); break;
watch_buzzer_play_note(BUZZER_NOTE_REST, 30); case EVENT_MODE_LONG_PRESS:
watch_buzzer_play_note(BUZZER_NOTE_E6, 30); if (tally_face_should_move_back(state)) {
print_tally(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
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; break;
case EVENT_ACTIVATE: case EVENT_ACTIVATE:
print_tally(state); print_tally(state, settings->bit.button_should_sound);
break; break;
case EVENT_TIMEOUT: case EVENT_TIMEOUT:
// ignore 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. // 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]; 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); watch_display_string(buf, 0);
} }

View file

@ -29,25 +29,41 @@
* TALLY face * TALLY face
* *
* Tally face is designed to act as a tally counter. * 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. * Alarm
* To reset, long press the ALARM button. * 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" #include "movement.h"
typedef struct { typedef struct {
uint32_t tally_idx; int16_t tally_idx;
uint8_t tally_default_idx;
} tally_state_t; } 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_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
void tally_face_activate(movement_settings_t *settings, void *context); void tally_face_activate(movement_settings_t *settings, void *context);
bool tally_face_loop(movement_event_t event, 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 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){ \ #define tally_face ((const watch_face_t){ \
tally_face_setup, \ tally_face_setup, \

View file

@ -38,12 +38,15 @@ void _wake_face_update_display(movement_settings_t *settings, wake_face_state_t
uint8_t hour = state->hour; uint8_t hour = state->hour;
watch_clear_display(); watch_clear_display();
if ( settings->bit.clock_mode_24h ) bool set_leading_zero = false;
watch_set_indicator(WATCH_INDICATOR_24H); if ( !settings->bit.clock_mode_24h ) {
else {
if ( hour >= 12 ) if ( hour >= 12 )
watch_set_indicator(WATCH_INDICATOR_PM); watch_set_indicator(WATCH_INDICATOR_PM);
hour = hour % 12 ? hour % 12 : 12; 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 ) if ( state->mode )
@ -54,6 +57,8 @@ void _wake_face_update_display(movement_settings_t *settings, wake_face_state_t
watch_set_colon(); watch_set_colon();
watch_display_string(lcdbuf, 0); watch_display_string(lcdbuf, 0);
if ( set_leading_zero )
watch_display_string("0", 4);
} }
// //

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

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

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

View 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;
beeps_state_t *state = (beeps_state_t *)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;
}

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

View file

@ -41,6 +41,7 @@ static void _lis2dw_logging_face_update_display(movement_settings_t *settings, l
char time_indication_character; char time_indication_character;
int8_t pos; int8_t pos;
watch_date_time date_time; watch_date_time date_time;
bool set_leading_zero = false;
if (logger_state->log_ticks) { if (logger_state->log_ticks) {
pos = (logger_state->data_points - 1 - logger_state->display_index) % LIS2DW_LOGGING_NUM_DATA_POINTS; 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 { } else {
date_time = logger_state->data[pos].timestamp; date_time = logger_state->data[pos].timestamp;
watch_set_colon(); watch_set_colon();
if (settings->bit.clock_mode_24h) { if (!settings->bit.clock_mode_24h) {
watch_set_indicator(WATCH_INDICATOR_24H);
} else {
if (date_time.unit.hour > 11) watch_set_indicator(WATCH_INDICATOR_PM); if (date_time.unit.hour > 11) watch_set_indicator(WATCH_INDICATOR_PM);
date_time.unit.hour %= 12; date_time.unit.hour %= 12;
if (date_time.unit.hour == 0) 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) { switch (logger_state->axis_index) {
case 0: case 0:
@ -89,6 +92,8 @@ static void _lis2dw_logging_face_update_display(movement_settings_t *settings, l
logger_state->interrupts[2]); logger_state->interrupts[2]);
} }
watch_display_string(buf, 0); watch_display_string(buf, 0);
if (set_leading_zero)
watch_display_string("0", 4);
} }
static void _lis2dw_logging_face_log_data(lis2dw_logger_state_t *logger_state) { static void _lis2dw_logging_face_log_data(lis2dw_logger_state_t *logger_state) {

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

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

View file

@ -40,9 +40,10 @@ static void _thermistor_logging_face_log_data(thermistor_logger_state_t *logger_
thermistor_driver_disable(); 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; int8_t pos = (logger_state->data_points - 1 - logger_state->display_index) % THERMISTOR_LOGGING_NUM_DATA_POINTS;
char buf[14]; char buf[14];
bool set_leading_zero = false;
watch_clear_indicator(WATCH_INDICATOR_24H); watch_clear_indicator(WATCH_INDICATOR_24H);
watch_clear_indicator(WATCH_INDICATOR_PM); 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) { } else if (logger_state->ts_ticks) {
watch_date_time date_time = logger_state->data[pos].timestamp; watch_date_time date_time = logger_state->data[pos].timestamp;
watch_set_colon(); watch_set_colon();
if (clock_mode_24h) { if (!clock_mode_24h) {
watch_set_indicator(WATCH_INDICATOR_24H);
} else {
if (date_time.unit.hour > 11) watch_set_indicator(WATCH_INDICATOR_PM); if (date_time.unit.hour > 11) watch_set_indicator(WATCH_INDICATOR_PM);
date_time.unit.hour %= 12; date_time.unit.hour %= 12;
if (date_time.unit.hour == 0) 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); sprintf(buf, "AT%2d%2d%02d%02d", date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
} else { } else {
@ -70,6 +73,8 @@ static void _thermistor_logging_face_update_display(thermistor_logger_state_t *l
} }
watch_display_string(buf, 0); 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) { 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; break;
case EVENT_LIGHT_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_DOWN:
logger_state->ts_ticks = 2; 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; break;
case EVENT_ALARM_BUTTON_DOWN: case EVENT_ALARM_BUTTON_DOWN:
logger_state->display_index = (logger_state->display_index + 1) % THERMISTOR_LOGGING_NUM_DATA_POINTS; logger_state->display_index = (logger_state->display_index + 1) % THERMISTOR_LOGGING_NUM_DATA_POINTS;
logger_state->ts_ticks = 0; logger_state->ts_ticks = 0;
// fall through // fall through
case EVENT_ACTIVATE: 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; break;
case EVENT_TICK: case EVENT_TICK:
if (logger_state->ts_ticks && --logger_state->ts_ticks == 0) { 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; break;
case EVENT_BACKGROUND_TASK: case EVENT_BACKGROUND_TASK:

View file

@ -84,6 +84,9 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings
break; break;
case 4: case 4:
settings->bit.led_duration = settings->bit.led_duration + 1; settings->bit.led_duration = settings->bit.led_duration + 1;
if (settings->bit.led_duration > 3) {
settings->bit.led_duration = 0b111;
}
break; break;
case 5: case 5:
settings->bit.led_green_color = settings->bit.led_green_color + 1; settings->bit.led_green_color = settings->bit.led_green_color + 1;
@ -93,6 +96,14 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings
break; break;
} }
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: case EVENT_TIMEOUT:
movement_move_to_face(0); movement_move_to_face(0);
break; break;
@ -109,8 +120,10 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings
char buf[8]; char buf[8];
switch (current_page) { switch (current_page) {
case 0: case 0:
if (settings->bit.clock_mode_24h) watch_display_string("24h", 4); if (settings->bit.clock_mode_24h) {
else watch_display_string("12h", 4); 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; break;
case 1: case 1:
if (settings->bit.button_should_sound) watch_display_string("y", 9); if (settings->bit.button_should_sound) watch_display_string("y", 9);
@ -161,11 +174,13 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings
} }
break; break;
case 4: 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); sprintf(buf, " %1d SeC", settings->bit.led_duration * 2 - 1);
watch_display_string(buf, 4); watch_display_string(buf, 4);
} else {
watch_display_string("no LEd", 4);
} }
break; break;
case 5: case 5:

View file

@ -126,10 +126,14 @@ bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, v
} }
char buf[11]; char buf[11];
bool set_leading_zero = false;
if (current_page < 3) { if (current_page < 3) {
watch_set_colon(); watch_set_colon();
if (settings->bit.clock_mode_24h) { if (settings->bit.clock_mode_24h) {
watch_set_indicator(WATCH_INDICATOR_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); 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 { } 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); 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);
@ -170,6 +174,8 @@ bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, v
} }
watch_display_string(buf, 0); watch_display_string(buf, 0);
if (set_leading_zero)
watch_display_string("0", 4);
return true; return true;
} }

View file

@ -189,10 +189,14 @@ bool set_time_hackwatch_face_loop(movement_event_t event, movement_settings_t *s
} }
char buf[11]; char buf[11];
bool set_leading_zero = false;
if (current_page < 3) { if (current_page < 3) {
watch_set_colon(); watch_set_colon();
if (settings->bit.clock_mode_24h) { if (settings->bit.clock_mode_24h) {
watch_set_indicator(WATCH_INDICATOR_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, sprintf(buf,
"%s %2d%02d%02d", "%s %2d%02d%02d",
set_time_hackwatch_face_titles[current_page], set_time_hackwatch_face_titles[current_page],
@ -258,6 +262,8 @@ bool set_time_hackwatch_face_loop(movement_event_t event, movement_settings_t *s
} }
watch_display_string(buf, 0); watch_display_string(buf, 0);
if (set_leading_zero)
watch_display_string("0", 4);
return true; return true;
} }

File diff suppressed because it is too large Load diff

View file

@ -89,6 +89,7 @@ void main_loop_sleep(uint32_t ms) {
main_loop_set_sleeping(true); main_loop_set_sleeping(true);
emscripten_sleep(ms); emscripten_sleep(ms);
main_loop_set_sleeping(false); main_loop_set_sleeping(false);
animation_frame_id = ANIMATION_FRAME_ID_INVALID;
} }
bool main_loop_is_sleeping(void) { bool main_loop_is_sleeping(void) {