diff --git a/Makefile b/Makefile index 5d1fbd2f..cae2b943 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,8 @@ TINYUSB_CDC=1 # Now we're all set to include gossamer's make rules. include $(GOSSAMER_PATH)/make.mk +CFLAGS+=-D_POSIX_C_SOURCE=200112L + define n diff --git a/legacy/watch_faces/clock/world_clock2_face.c b/legacy/watch_faces/clock/world_clock2_face.c index e79f2df7..e47bc44d 100644 --- a/legacy/watch_faces/clock/world_clock2_face.c +++ b/legacy/watch_faces/clock/world_clock2_face.c @@ -28,7 +28,6 @@ #include "world_clock2_face.h" #include "watch.h" #include "watch_utility.h" -#include "watch_utility.h" static bool refresh_face; diff --git a/movement.c b/movement.c index 8e2c5031..e81ccb94 100644 --- a/movement.c +++ b/movement.c @@ -27,9 +27,7 @@ #include #include #include -#include #include -#include #include "app.h" #include "watch.h" #include "watch_utility.h" @@ -51,11 +49,12 @@ #if __EMSCRIPTEN__ #include +void _wake_up_simulator(void); #else #include "watch_usb_cdc.h" #endif -movement_state_t movement_state; +volatile movement_state_t movement_state; void * watch_face_contexts[MOVEMENT_NUM_FACES]; watch_date_time_t scheduled_tasks[MOVEMENT_NUM_FACES]; const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800}; @@ -515,18 +514,18 @@ bool movement_enable_tap_detection_if_available(void) { if (movement_state.has_lis2dw) { // configure tap duration threshold and enable Z axis lis2dw_configure_tap_threshold(0, 0, 12, LIS2DW_REG_TAP_THS_Z_Z_AXIS_ENABLE); - lis2dw_configure_tap_duration(10, 2, 2); + lis2dw_configure_tap_duration(2, 2, 2); // ramp data rate up to 400 Hz and high performance mode lis2dw_set_low_noise_mode(true); lis2dw_set_data_rate(LIS2DW_DATA_RATE_HP_400_HZ); - lis2dw_set_mode(LIS2DW_MODE_HIGH_PERFORMANCE); + lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); // Settling time (1 sample duration, i.e. 1/400Hz) delay_ms(3); // enable tap detection on INT1/A3. - lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_6D); + lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_DOUBLE_TAP); return true; } @@ -540,6 +539,7 @@ bool movement_disable_tap_detection_if_available(void) { lis2dw_set_low_noise_mode(false); lis2dw_set_data_rate(movement_state.accelerometer_background_rate); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); + lis2dw_disable_double_tap(); // ...disable Z axis (not sure if this is needed, does this save power?)... lis2dw_configure_tap_threshold(0, 0, 0, 0); @@ -587,6 +587,11 @@ bool movement_set_accelerometer_motion_threshold(uint8_t new_threshold) { float movement_get_temperature(void) { float temperature_c = (float)0xFFFFFFFF; +#if __EMSCRIPTEN__ + temperature_c = EM_ASM_DOUBLE({ + return temp_c || 25.0; + }); +#else if (movement_state.has_thermistor) { thermistor_driver_enable(); @@ -597,6 +602,7 @@ float movement_get_temperature(void) { val = val >> 4; temperature_c = 25 + (float)val / 16.0; } +#endif return temperature_c; } @@ -609,13 +615,14 @@ void app_init(void) { // check if we are plugged into USB power. HAL_GPIO_VBUS_DET_in(); HAL_GPIO_VBUS_DET_pulldown(); + delay_ms(100); if (HAL_GPIO_VBUS_DET_read()){ /// if so, enable USB functionality. _watch_enable_usb(); } HAL_GPIO_VBUS_DET_off(); - memset(&movement_state, 0, sizeof(movement_state)); + memset((void *)&movement_state, 0, sizeof(movement_state)); movement_state.has_thermistor = thermistor_driver_init(); @@ -944,10 +951,14 @@ bool app_loop(void) { } } +#if __EMSCRIPTEN__ + shell_task(); +#else // if we are plugged into USB, handle the serial shell if (usb_is_enabled()) { shell_task(); } +#endif event.subsecond = 0; @@ -971,7 +982,7 @@ bool app_loop(void) { return can_sleep; } -static movement_event_type_t _figure_out_button_event(bool pin_level, movement_event_type_t button_down_event_type, uint16_t *down_timestamp) { +static movement_event_type_t _figure_out_button_event(bool pin_level, movement_event_type_t button_down_event_type, volatile uint16_t *down_timestamp) { // force alarm off if the user pressed a button. if (movement_state.alarm_ticks) movement_state.alarm_ticks = 0; @@ -1018,6 +1029,10 @@ void cb_alarm_btn_extwake(void) { } void cb_alarm_fired(void) { +#if __EMSCRIPTEN__ + _wake_up_simulator(); +#endif + movement_state.woke_from_alarm_handler = true; } diff --git a/movement_custom_signal_tunes.h b/movement_custom_signal_tunes.h index 96f083b4..a654433c 100644 --- a/movement_custom_signal_tunes.h +++ b/movement_custom_signal_tunes.h @@ -200,3 +200,57 @@ int8_t signal_tune[] = { 0 }; #endif // SIGNAL_TUNE_HARRY_POTTER_LONG + +#ifdef SIGNAL_TUNE_JURASSIC_PARK +int8_t signal_tune[] = { + BUZZER_NOTE_B5, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_A5SHARP_B5FLAT, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_B5, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_F5SHARP_G5FLAT, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_E5, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_B5, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_A5SHARP_B5FLAT, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_B5, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_F5SHARP_G5FLAT, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_E5, 13, + 0, +}; +#endif // SIGNAL_TUNE_JURASSIC_PARK + +#ifdef SIGNAL_TUNE_EVANGELION +int8_t signal_tune[] = { + BUZZER_NOTE_C5, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_D5SHARP_E5FLAT, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_F5, 13, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_D5SHARP_E5FLAT, 13, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_F5, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_F5, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_F5, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_A5SHARP_B5FLAT, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_G5SHARP_A5FLAT, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_G5, 3, + BUZZER_NOTE_REST, 3, + BUZZER_NOTE_F5, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_G5, 13, + 0, +}; +#endif // SIGNAL_TUNE_EVANGELION diff --git a/movement_faces.h b/movement_faces.h index 3583e9a3..eef0e3ac 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -59,7 +59,23 @@ #include "periodic_table_face.h" #include "squash_face.h" #include "totp_face.h" +#include "totp_lfs_face.h" #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" +#include "baby_kicks_face.h" +#include "counter_face.h" +#include "pulsometer_face.h" +#include "interval_face.h" +#include "timer_face.h" +#include "simple_coin_flip_face.h" +#include "lis2dw_monitor_face.h" +#include "wareki_face.h" +#include "deadline_face.h" +#include "wordle_face.h" +#include "blackjack_face.h" +#include "endless_runner_face.h" +#include "higher_lower_game_face.h" +#include "lander_face.h" +#include "simon_face.h" // New includes go above this line. diff --git a/template/template.c b/template/template.c index 4f2e7e1b..cf51d778 100644 --- a/template/template.c +++ b/template/template.c @@ -81,11 +81,6 @@ bool <#watch_face_name#>_face_loop(movement_event_t event, void *context) { } // 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; } diff --git a/watch-faces.mk b/watch-faces.mk index eff50d27..3c424923 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -15,7 +15,9 @@ SRCS += \ ./watch-faces/complication/breathing_face.c \ ./watch-faces/complication/squash_face.c \ ./watch-faces/complication/totp_face.c \ + ./watch-faces/complication/totp_lfs_face.c \ ./watch-faces/complication/tally_face.c \ + ./watch-faces/complication/wordle_face.c \ ./watch-faces/demo/all_segments_face.c \ ./watch-faces/demo/character_set_face.c \ ./watch-faces/demo/light_sensor_face.c \ @@ -37,4 +39,18 @@ SRCS += \ ./watch-faces/complication/kitchen_conversions_face.c \ ./watch-faces/complication/periodic_table_face.c \ ./watch-faces/clock/ke_decimal_time_face.c \ + ./watch-faces/complication/baby_kicks_face.c \ + ./watch-faces/complication/counter_face.c \ + ./watch-faces/complication/pulsometer_face.c \ + ./watch-faces/complication/interval_face.c \ + ./watch-faces/complication/timer_face.c \ + ./watch-faces/complication/simple_coin_flip_face.c \ + ./watch-faces/sensor/lis2dw_monitor_face.c \ + ./watch-faces/complication/wareki_face.c \ + ./watch-faces/complication/deadline_face.c \ + ./watch-faces/complication/blackjack_face.c \ + ./watch-faces/complication/endless_runner_face.c \ + ./watch-faces/complication/higher_lower_game_face.c \ + ./watch-faces/complication/lander_face.c \ + ./watch-faces/complication/simon_face.c \ # New watch faces go above this line. diff --git a/watch-faces/clock/ke_decimal_time_face.c b/watch-faces/clock/ke_decimal_time_face.c index 6737477c..c2e9961b 100644 --- a/watch-faces/clock/ke_decimal_time_face.c +++ b/watch-faces/clock/ke_decimal_time_face.c @@ -36,14 +36,14 @@ static void _display_date(watch_date_time_t date_time) { } static void _display_time(ke_decimal_time_state_t *state, watch_date_time_t date_time, bool low_energy) { - char buf[7]; + char buf[8]; uint32_t value = date_time.unit.hour * 3600 + date_time.unit.minute * 60 + date_time.unit.second; if (value == state->previous_time) return; value = value * 100; value = value / 864; - snprintf(buf, sizeof(buf), "%04d#o", value); + snprintf(buf, sizeof(buf), "%04ld#o", value); // if under 10%, display 0.00 instead of 00.00 if (value < 1000) buf[0] = ' '; @@ -69,6 +69,10 @@ void ke_decimal_time_face_setup(uint8_t watch_face_index, void ** context_ptr) { void ke_decimal_time_face_activate(void *context) { ke_decimal_time_state_t *state = (ke_decimal_time_state_t *)context; + if (watch_sleep_animation_is_running()) { + watch_stop_sleep_animation(); + } + // force re-display of date and time in EVENT_ACTIVATE state->previous_day = 0xFF; state->previous_time = 0xFFFFFFFF; @@ -126,11 +130,6 @@ bool ke_decimal_time_face_loop(movement_event_t event, void *context) { } // 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; } diff --git a/watch-faces/complication/advanced_alarm_face.c b/watch-faces/complication/advanced_alarm_face.c index 39739bc4..16bfdbce 100644 --- a/watch-faces/complication/advanced_alarm_face.c +++ b/watch-faces/complication/advanced_alarm_face.c @@ -40,9 +40,9 @@ typedef enum { alarm_setting_idx_beeps } alarm_setting_idx_t; -static const char _dow_strings[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SO", "ED", "1t", "MF", "WN"}; -static const uint8_t _blink_idx[ALARM_SETTING_STATES] = {2, 0, 4, 6, 8, 9}; -static const uint8_t _blink_idx2[ALARM_SETTING_STATES] = {3, 1, 5, 7, 8, 9}; +static const char _dow_strings_classic[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SU", "ED", "1t", "MF", "WN"}; +static const char _dow_strings_custom[ALARM_DAY_STATES + 1][3] ={ "AL ", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN", "DAY", "1t ", "M-F", "WKD"}; +static const uint8_t _beeps_blink_idx = 9; static const watch_buzzer_note_t _buzzer_notes[3] = {BUZZER_NOTE_B6, BUZZER_NOTE_C8, BUZZER_NOTE_A8}; // Volume is indicated by the three segments 5D, 5G and 5A @@ -67,6 +67,10 @@ static void _alarm_set_signal(alarm_state_t *state) { watch_clear_indicator(WATCH_INDICATOR_SIGNAL); } +static void _alarm_show_alarm_on_text(alarm_state_t *state) { + watch_display_text(WATCH_POSITION_SECONDS, state->alarm[state->alarm_idx].enabled ? "on" : "--"); +} + static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) { char buf[12]; bool set_leading_zero = movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_024H; @@ -90,18 +94,34 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) { watch_set_indicator(WATCH_INDICATOR_24H); } - sprintf(buf, set_leading_zero? "%c%c%2d%02d%02d " : "%c%c%2d%2d%02d ", - _dow_strings[i][0], _dow_strings[i][1], - (state->alarm_idx + 1), - h, - state->alarm[state->alarm_idx].minute); // blink items if in settings mode - if (state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks) { - buf[_blink_idx[state->setting_state]] = buf[_blink_idx2[state->setting_state]] = ' '; + bool blinking = state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks; + if (state->setting_state == alarm_setting_idx_alarm && blinking) { + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + } else { + sprintf(buf, "%2d", (state->alarm_idx + 1)); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); + } + if (state->setting_state == alarm_setting_idx_day && blinking) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, " ", " "); + } else { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, _dow_strings_custom[i], _dow_strings_classic[i]); + } + if (state->setting_state == alarm_setting_idx_hour && blinking) { + watch_display_text(WATCH_POSITION_HOURS, " "); + } else { + sprintf(buf, set_leading_zero? "%02d" : "%2d", h); + watch_display_text(WATCH_POSITION_HOURS, buf); + } + if (state->setting_state == alarm_setting_idx_minute && blinking) { + watch_display_text(WATCH_POSITION_MINUTES, " "); + } else { + sprintf(buf, "%02d", state->alarm[state->alarm_idx].minute); + watch_display_text(WATCH_POSITION_MINUTES, buf); } - watch_display_text(WATCH_POSITION_FULL, buf); if (state->is_setting) { + watch_display_text(WATCH_POSITION_SECONDS, " "); // draw pitch level indicator if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_pitch)) { for (i = 0; i <= state->alarm[state->alarm_idx].pitch && i < 3; i++) @@ -110,15 +130,18 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) { // draw beep rounds indicator if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_beeps)) { if (state->alarm[state->alarm_idx].beeps == ALARM_MAX_BEEP_ROUNDS - 1) - watch_display_character('L', _blink_idx[alarm_setting_idx_beeps]); + watch_display_character('L', _beeps_blink_idx); else { if (state->alarm[state->alarm_idx].beeps == 0) - watch_display_character('o', _blink_idx[alarm_setting_idx_beeps]); + watch_display_character('o', _beeps_blink_idx); else - watch_display_character(state->alarm[state->alarm_idx].beeps + 48, _blink_idx[alarm_setting_idx_beeps]); + watch_display_character(state->alarm[state->alarm_idx].beeps + 48, _beeps_blink_idx); } } } + else { + _alarm_show_alarm_on_text(state); + } // set alarm indicator _alarm_set_signal(state); @@ -303,6 +326,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) { // revert change of enabled flag and show it briefly state->alarm[state->alarm_idx].enabled ^= 1; _alarm_set_signal(state); + _alarm_show_alarm_on_text(state); delay_ms(275); state->alarm_idx = 0; } diff --git a/watch-faces/complication/baby_kicks_face.c b/watch-faces/complication/baby_kicks_face.c new file mode 100644 index 00000000..cc0dd513 --- /dev/null +++ b/watch-faces/complication/baby_kicks_face.c @@ -0,0 +1,439 @@ +/* + * MIT License + * + * Copyright (c) 2025 Gábor Nyéki + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include "baby_kicks_face.h" +#include "watch.h" +#include "watch_utility.h" + +static inline void _play_failure_sound_if_beep_is_on() { + if (movement_button_should_sound()) { + watch_buzzer_play_note(BUZZER_NOTE_E7, 10); + } +} + +static inline void _play_successful_increment_sound_if_beep_is_on() { + if (movement_button_should_sound()) { + watch_buzzer_play_note(BUZZER_NOTE_E6, 10); + } +} + +static inline void _play_successful_decrement_sound_if_beep_is_on() { + if (movement_button_should_sound()) { + watch_buzzer_play_note(BUZZER_NOTE_D6, 10); + } +} + +static inline void _play_button_sound_if_beep_is_on() { + if (movement_button_should_sound()) { + watch_buzzer_play_note(BUZZER_NOTE_C7, 10); + } +} + +/** @brief Predicate for whether the counter has been started. + */ +static inline bool _is_running(baby_kicks_state_t *state) { + return state->start > 0; +} + +/** @brief Gets the current time, and caches it for re-use. + */ +static inline watch_date_time_t *_get_now(baby_kicks_state_t *state) { + if (state->now.unit.year == 0) { + state->now = movement_get_local_date_time(); + } + + return &state->now; +} + +/** @brief Clears the current time. Should only be called at the end of + * `baby_kicks_face_loop`. + */ +static inline void _clear_now(baby_kicks_state_t *state) { + if (state->now.unit.year > 0) { + memset(&state->now, 0, sizeof(state->now)); + } +} + +/** @brief Calculates the number of minutes since the timer was started. + * @return If the counter has been started, then the number of full + * minutes that have elapsed. If it has not been started, then + * 255. + */ +static inline uint32_t _elapsed_minutes(baby_kicks_state_t *state) { + if (!_is_running(state)) { + return 0xff; + } + + watch_date_time_t *now = _get_now(state); + + return ( + watch_utility_date_time_to_unix_time(*now, 0) - state->start + ) / 60; +} + +/** @brief Predicate for whether the counter has started but run for too + * long. + */ +static inline bool _has_timed_out(baby_kicks_state_t *state) { + return _elapsed_minutes(state) > BABY_KICKS_TIMEOUT; +} + +/** @brief Determines what we should display based on `state`. Should + * only be called from `baby_kicks_face_loop`. + */ +static void _update_display_mode(baby_kicks_state_t *state) { + if (watch_sleep_animation_is_running()) { + state->mode = BABY_KICKS_MODE_LE_MODE; + } else if (!_is_running(state)) { + state->mode = BABY_KICKS_MODE_SPLASH; + } else if (_has_timed_out(state)) { + state->mode = BABY_KICKS_MODE_TIMED_OUT; + } else { + state->mode = BABY_KICKS_MODE_ACTIVE; + } +} + +/** @brief Starts the counter. + * @details Sets the start time which will be used to calculate the + * elapsed minutes. + */ +static inline void _start(baby_kicks_state_t *state) { + watch_date_time_t *now = _get_now(state); + uint32_t now_unix = watch_utility_date_time_to_unix_time(*now, 0); + + state->start = now_unix; +} + +/** @brief Resets the counter. + * @details Zeros out the watch face state and clears the undo ring + * buffer. Effectively sets `state->mode` to + * `BABY_KICKS_MODE_SPLASH`. + */ +static void _reset(baby_kicks_state_t *state) { + memset(state, 0, sizeof(baby_kicks_state_t)); + memset( + state->undo_buffer.stretches, + 0xff, + sizeof(state->undo_buffer.stretches) + ); +} + +/** @brief Records a movement. + * @details Increments the movement counter, and along with it, if + * necessary, the counter of one-minute stretches. Also adds + * the movement to the undo buffer. + */ +static inline void _increment_counts(baby_kicks_state_t *state) { + watch_date_time_t *now = _get_now(state); + uint32_t now_unix = watch_utility_date_time_to_unix_time(*now, 0); + + /* Add movement to the undo ring buffer. */ + state->undo_buffer.stretches[state->undo_buffer.head] = + state->stretch_count; + state->undo_buffer.head++; + state->undo_buffer.head %= sizeof(state->undo_buffer.stretches); + + state->movement_count++; + + if (state->stretch_count == 0 + || state->latest_stretch_start + 60 < now_unix) { + /* Start new stretch. */ + state->latest_stretch_start = now_unix; + state->stretch_count++; + } +} + +/** @brief Undoes the last movement. + * @details Decrements the movement counter and, if necessary, the + * counter of one-minute stretches. + * @return True if and only if there was a movement to undo. + */ +static inline bool _successfully_undo(baby_kicks_state_t *state) { + uint8_t latest_mvmt, pre_undo_stretch_count; + + /* The latest movement is stored one position before `.head`. */ + if (state->undo_buffer.head == 0) { + latest_mvmt = sizeof(state->undo_buffer.stretches) - 1; + } else { + latest_mvmt = state->undo_buffer.head - 1; + } + + pre_undo_stretch_count = + state->undo_buffer.stretches[latest_mvmt]; + + if (pre_undo_stretch_count == 0xff) { + /* Nothing to undo. */ + return false; + } else if (pre_undo_stretch_count < state->stretch_count) { + state->latest_stretch_start = 0; + state->stretch_count--; + } + + state->movement_count--; + + state->undo_buffer.stretches[latest_mvmt] = 0xff; + state->undo_buffer.head = latest_mvmt; + + return true; +} + +/** @brief Updates the display with the movement counts if the counter + * has been started. + */ +static inline void _display_counts(baby_kicks_state_t *state) { + if (!_is_running(state)) { + watch_display_text(WATCH_POSITION_BOTTOM, "baby "); + watch_clear_colon(); + } else { + char buf[9]; + + snprintf( + buf, + sizeof(buf), + "%2d%4d", + state->stretch_count, + state->movement_count + ); + + watch_display_text(WATCH_POSITION_BOTTOM, buf); + watch_set_colon(); + } +} + +/** @brief Updates the display with the number of minutes since the + * timer was started. + * @details If more than `BABY_KICKS_TIMEOUT` minutes have elapsed, + * then it displays "TO". + */ +static void _display_elapsed_minutes(baby_kicks_state_t *state) { + if (!_is_running(state)) { + watch_display_text(WATCH_POSITION_TOP_LEFT, " "); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + } else if (_has_timed_out(state)) { + watch_display_text(WATCH_POSITION_TOP_LEFT, "TO"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + } else { + /* NOTE We display the elapsed minutes in two parts. This is + * because on the classic LCD, neither the "weekday digits" nor + * the "day digits" position is suitable to display the elapsed + * minutes: + * + * - The classic LCD cannot display 2, 4, 5, 6, or 9 as the last + * digit in the "weekday digits" position. + * - It cannot display any number greater than 3 as the first + * digit in the "day digits" position. + * + * As a workaround, we split the elapsed minutes into 30-minute + * "laps." The elapsed minutes in the current "lap" are shown + * in the "day digits" position. This is any number between 0 + * and 29. The elapsed minutes in past "laps" are shown in the + * "weekday digits" position. This is either nothing, 30, 60, + * or 90. + * + * The sum of the numbers shown in the two positions is equal to + * the total elapsed minutes. + */ + + char buf[5]; + uint32_t elapsed_minutes = _elapsed_minutes(state); + uint8_t multiple = elapsed_minutes / 30; + uint8_t remainder = elapsed_minutes % 30; + + if (multiple == 0) { + watch_display_text(WATCH_POSITION_TOP_LEFT, " "); + } else { + snprintf(buf, sizeof(buf), "%2d", multiple * 30); + watch_display_text(WATCH_POSITION_TOP_LEFT, buf); + } + + snprintf(buf, sizeof(buf), "%2d", remainder); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); + } +} + +static void _update_display(baby_kicks_state_t *state) { + _display_counts(state); + _display_elapsed_minutes(state); +} + +static inline void _start_sleep_face() { + if (!watch_sleep_animation_is_running()) { + watch_display_text(WATCH_POSITION_TOP_LEFT, " "); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, "baby "); + + watch_clear_colon(); + + watch_start_sleep_animation(500); + } +} + +static inline void _stop_sleep_face() { + if (watch_sleep_animation_is_running()) { + watch_stop_sleep_animation(); + } +} + +void baby_kicks_face_setup(uint8_t watch_face_index, + void **context_ptr) { + (void) watch_face_index; + + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(baby_kicks_state_t)); + _reset(*context_ptr); + } +} + +void baby_kicks_face_activate(void *context) { + (void) context; + + _stop_sleep_face(); +} + +void baby_kicks_face_resign(void *context) { + baby_kicks_state_t *state = (baby_kicks_state_t *)context; + + state->currently_displayed = false; +} + +bool baby_kicks_face_loop(movement_event_t event, void *context) { + baby_kicks_state_t *state = (baby_kicks_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + state->currently_displayed = true; + _update_display_mode(state); + _update_display(state); + break; + case EVENT_ALARM_BUTTON_UP: /* Start or increment. */ + /* Update `state->mode` in case we have a running counter + * that has just timed out. */ + _update_display_mode(state); + + switch (state->mode) { + case BABY_KICKS_MODE_SPLASH: + _start(state); + _update_display_mode(state); + _update_display(state); + _play_button_sound_if_beep_is_on(); + break; + case BABY_KICKS_MODE_ACTIVE: + _increment_counts(state); + _update_display(state); + _play_successful_increment_sound_if_beep_is_on(); + break; + case BABY_KICKS_MODE_TIMED_OUT: + _play_failure_sound_if_beep_is_on(); + break; + case BABY_KICKS_MODE_LE_MODE: /* fallthrough */ + default: + break; + } + break; + case EVENT_ALARM_LONG_PRESS: /* Undo. */ + _update_display_mode(state); + + switch (state->mode) { + case BABY_KICKS_MODE_ACTIVE: + if (!_successfully_undo(state)) { + _play_failure_sound_if_beep_is_on(); + } else { + _update_display(state); + _play_successful_decrement_sound_if_beep_is_on(); + } + break; + case BABY_KICKS_MODE_SPLASH: /* fallthrough */ + case BABY_KICKS_MODE_TIMED_OUT: + _play_failure_sound_if_beep_is_on(); + break; + case BABY_KICKS_MODE_LE_MODE: /* fallthrough */ + default: + break; + } + break; + case EVENT_MODE_LONG_PRESS: /* Reset. */ + _update_display_mode(state); + + switch (state->mode) { + case BABY_KICKS_MODE_ACTIVE: /* fallthrough */ + case BABY_KICKS_MODE_TIMED_OUT: + _reset(state); + + /* This shows the splash screen because `_reset` + * sets `state->mode` to `BABY_KICKS_MODE_SPLASH`. + */ + _update_display(state); + + _play_button_sound_if_beep_is_on(); + break; + case BABY_KICKS_MODE_SPLASH: + _play_failure_sound_if_beep_is_on(); + break; + case BABY_KICKS_MODE_LE_MODE: /* fallthrough */ + default: + break; + } + break; + case EVENT_BACKGROUND_TASK: /* Update minute display. */ + _update_display_mode(state); + + switch (state->mode) { + case BABY_KICKS_MODE_ACTIVE: /* fallthrough */ + case BABY_KICKS_MODE_TIMED_OUT: + if (state->currently_displayed) { + _display_elapsed_minutes(state); + } + break; + case BABY_KICKS_MODE_LE_MODE: /* fallthrough */ + case BABY_KICKS_MODE_SPLASH: /* fallthrough */ + default: + break; + } + break; + case EVENT_LOW_ENERGY_UPDATE: + _start_sleep_face(); + break; + default: + movement_default_loop_handler(event); + break; + } + + _clear_now(state); + + return true; +} + +movement_watch_face_advisory_t baby_kicks_face_advise(void *context) { + movement_watch_face_advisory_t retval = { 0 }; + baby_kicks_state_t *state = (baby_kicks_state_t *)context; + + retval.wants_background_task = + state->mode == BABY_KICKS_MODE_ACTIVE; + + return retval; +} diff --git a/watch-faces/complication/baby_kicks_face.h b/watch-faces/complication/baby_kicks_face.h new file mode 100644 index 00000000..c20a082a --- /dev/null +++ b/watch-faces/complication/baby_kicks_face.h @@ -0,0 +1,132 @@ +/* + * MIT License + * + * Copyright (c) 2025 Gábor Nyéki + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +/* + * Baby kicks face + * + * Count the movements of your in-utero baby. + * + * Background: + * + * This practice is recommended particularly in the third trimester + * (from week 28 onwards). The exact recommendations vary as to how to + * count the baby's movements. Some recommend drawing a chart with the + * number of "kicks" within a 12-hour period: + * + * - https://en.wikipedia.org/wiki/Kick_chart + * + * Others recommend measuring the time that it takes for the baby to + * "kick" 10 times: + * + * - https://my.clevelandclinic.org/health/articles/23497-kick-counts + * - https://healthy.kaiserpermanente.org/health-wellness/health-encyclopedia/he.pregnancy-kick-counts.aa107042 + * + * (Of course, not every movement that the baby makes is a kick, and we + * are interested in all movements, not only kicks.) + * + * This watch face follows the latter set of recommendations. When you + * start the counter, it measures the number of elapsed minutes, and it + * tracks the number of movements as you increment the counter. Since + * some consecutive movements made by the baby are actually part of a + * longer maneuver, the watch face also displays the number of + * one-minute stretches in which the baby moved at least once. + * + * Usage: + * + * - ALARM button, short press: + * * start the counter if it isn't running + * * increment the count otherwise + * - ALARM button, long press: undo the last count + * - MODE button, long press: reset the count to zero + * + * The watch face displays two numbers in the "clock digits" positions: + * + * 1. Count of movements (in the "second" and "minute" positions). + * 2. Count of one-minute stretches in which at least one movement + * occurred (in the "hour" position). + * + * The number of elapsed minutes, up to and including 29, is shown in + * the "day digits" position. Due to the limitations of the classic LCD + * display, completed 30-minute intervals are shown in the "weekday + * digits" position. The total number of elapsed minutes is the sum of + * these two numbers. + * + * The watch face times out after 99 minutes, since it cannot display + * more than 99 one-minute stretches in the "hour" position. When this + * happens, the "weekday digits" position shows "TO". + */ + +#include "movement.h" + +typedef enum { + BABY_KICKS_MODE_SPLASH = 0, + BABY_KICKS_MODE_ACTIVE, + BABY_KICKS_MODE_TIMED_OUT, + BABY_KICKS_MODE_LE_MODE, +} baby_kicks_mode_t; + +/* Stop counting after 99 minutes. The classic LCD cannot display any + * larger number in the "weekday digits" position. */ +#define BABY_KICKS_TIMEOUT 99 + +/* Ring buffer to store and allow undoing up to 10 movements. */ +typedef struct { + /* For each movement in the undo buffer, this array stores the value + * of `state->stretch_count` right before the movement was + * recorded. This is used for decrementing `state->stretch_count` + * as part of the undo operation if necessary. */ + uint8_t stretches[10]; + + /* Index of the next available slot in `.stretches`. */ + uint8_t head; +} baby_kicks_undo_buffer_t; + +typedef struct { + bool currently_displayed; + baby_kicks_mode_t mode; + watch_date_time_t now; + uint32_t start; + uint32_t latest_stretch_start; + uint8_t stretch_count; /* Between 0 and `BABY_KICKS_TIMEOUT`. */ + uint16_t movement_count; /* Between 0 and 9999. */ + baby_kicks_undo_buffer_t undo_buffer; +} baby_kicks_state_t; + +void baby_kicks_face_setup(uint8_t watch_face_index, void **context_ptr); +void baby_kicks_face_activate(void *context); +bool baby_kicks_face_loop(movement_event_t event, void *context); +void baby_kicks_face_resign(void *context); +movement_watch_face_advisory_t baby_kicks_face_advise(void *context); + +#define baby_kicks_face ((const watch_face_t) { \ + baby_kicks_face_setup, \ + baby_kicks_face_activate, \ + baby_kicks_face_loop, \ + baby_kicks_face_resign, \ + baby_kicks_face_advise, \ +}) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c new file mode 100755 index 00000000..375e0435 --- /dev/null +++ b/watch-faces/complication/blackjack_face.c @@ -0,0 +1,467 @@ +/* + * MIT License + * + * Copyright (c) 2025 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. + */ + +// Emulator only: need time() to seed the random number generator. +#if __EMSCRIPTEN__ +#include +#endif + +#include +#include +#include "blackjack_face.h" +#include "watch_common_display.h" + +#define ACE 14 +#define KING 13 +#define QUEEN 12 +#define JACK 11 + +#define MIN_CARD_VALUE 2 +#define MAX_CARD_VALUE ACE +#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 BLACKJACK_MAX_HAND_SIZE 11 // 4*1 + 4*2 + 3*3 = 21; 11 cards total +#define MAX_PLAYER_CARDS_DISPLAY 4 +#define BOARD_DISPLAY_START 4 + +typedef struct { + uint8_t score; + uint8_t idx_hand; + int8_t high_aces_in_hand; + uint8_t hand[BLACKJACK_MAX_HAND_SIZE]; +} hand_info_t; + +typedef enum { + BJ_TITLE_SCREEN, + BJ_PLAYING, + BJ_DEALER_PLAYING, + BJ_BUST, + BJ_RESULT, + BJ_WIN_RATIO, +} game_state_t; + +typedef enum { + A, B, C, D, E, F, G +} segment_t; + +static bool tap_turned_on = false; +static game_state_t game_state; +static uint8_t deck[DECK_SIZE] = {0}; +static uint8_t current_card = 0; +static blackjack_face_state_t *g_state = NULL; +hand_info_t player; +hand_info_t dealer; + +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; + shuffle_deck(); +} + +static uint8_t get_next_card(void) { + if (current_card >= DECK_SIZE) + reset_deck(); + return deck[current_card++]; +} + +static uint8_t get_card_value(uint8_t card) { + switch (card) + { + case ACE: + return 11; + case KING: + case QUEEN: + case JACK: + return 10; + default: + return card; + } +} + +static void modify_score_from_aces(hand_info_t *hand_info) { + while (hand_info->score > 21 && hand_info->high_aces_in_hand > 0) { + hand_info->score -= 10; + hand_info->high_aces_in_hand--; + } +} + +static void reset_hands(void) { + memset(&player, 0, sizeof(player)); + memset(&dealer, 0, sizeof(dealer)); + reset_deck(); +} + +static void give_card(hand_info_t *hand_info) { + uint8_t card = get_next_card(); + if (card == ACE) hand_info->high_aces_in_hand++; + hand_info->hand[hand_info->idx_hand++] = card; + uint8_t card_value = get_card_value(card); + hand_info->score += card_value; + modify_score_from_aces(hand_info); +} + +static void set_segment_at_position(segment_t segment, uint8_t position) { + digit_mapping_t segmap; + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) { + segmap = Custom_LCD_Display_Mapping[position]; + } else { + segmap = Classic_LCD_Display_Mapping[position]; + } + const uint8_t com_pin = segmap.segment[segment].address.com; + const uint8_t seg = segmap.segment[segment].address.seg; + watch_set_pixel(com_pin, seg); +} + +static void display_card_at_position(uint8_t card, uint8_t display_position) { + switch (card) { + case KING: + 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 QUEEN: + watch_display_character(' ', display_position); + set_segment_at_position(A, display_position); + set_segment_at_position(D, display_position); + break; + case JACK: + watch_display_character('-', display_position); + break; + case ACE: + watch_display_character(watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM ? 'A' : 'a', display_position); + break; + case 10: + watch_display_character('0', display_position); + break; + default: { + const char display_char = card + '0'; + watch_display_character(display_char, display_position); + break; + } + } +} + +static void display_player_hand(void) { + uint8_t card; + if (player.idx_hand <= MAX_PLAYER_CARDS_DISPLAY) { + card = player.hand[player.idx_hand - 1]; + display_card_at_position(card, BOARD_DISPLAY_START + player.idx_hand - 1); + } else { + for (uint8_t i=0; igames_played++; + if (g_state->games_played == 0) { + // Overflow + g_state->games_played = 1; + g_state->games_won = add_to_wins ? 1 : 0; + return; + } + if (add_to_wins) { + g_state->games_won++; + if (g_state->games_won == 0) { + // Overflow + g_state->games_played = 1; + g_state->games_won = 1; + } + } +} + +static void display_win(void) { + game_state = BJ_RESULT; + add_to_game_scores(true); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WlN ", " WIN"); + display_score(player.score, WATCH_POSITION_SECONDS); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); +} + +static void display_lose(void) { + game_state = BJ_RESULT; + add_to_game_scores(false); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "LOSE", "lOSE"); + display_score(player.score, WATCH_POSITION_SECONDS); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); +} + +static void display_tie(void) { + game_state = BJ_RESULT; + // Don't record ties to the win ratio + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "TlE ", " TIE"); + display_score(player.score, WATCH_POSITION_SECONDS); +} + +static void display_bust(void) { + game_state = BJ_RESULT; + add_to_game_scores(false); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST", "BUST"); +} + +static void display_title(void) { + game_state = BJ_TITLE_SCREEN; + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "BLACK ", "21"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " JACK ", "BLaKJK"); +} + +static void display_win_ratio(blackjack_face_state_t *state) { + char buf[7]; + game_state = BJ_WIN_RATIO; + uint8_t win_ratio = 0; + if (state->games_played > 0) { // Avoid dividing by zero + win_ratio = (uint8_t)(100 * state->games_won) / state->games_played; + } + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "WINS ", "WR"); + sprintf(buf, "%3dPct", win_ratio); + watch_display_text(WATCH_POSITION_BOTTOM, buf); +} + +static void begin_playing(bool tap_control_on) { + watch_clear_display(); + if (tap_control_on) { + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } + game_state = BJ_PLAYING; + reset_hands(); + // Give player their first 2 cards + give_card(&player); + display_player_hand(); + give_card(&player); + display_player_hand(); + display_score(player.score, WATCH_POSITION_SECONDS); + give_card(&dealer); + display_dealer_hand(); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); +} + +static void perform_stand(void) { + game_state = BJ_DEALER_PLAYING; + watch_display_text(WATCH_POSITION_BOTTOM, "Stnd"); + display_score(player.score, WATCH_POSITION_SECONDS); +} + +static void perform_hit(void) { + if (player.score == 21) { + perform_stand(); + return; // Assume hitting on 21 is a mistake and stand + } + give_card(&player); + if (player.score > 21) { + game_state = BJ_BUST; + } + display_player_hand(); + display_score(player.score, WATCH_POSITION_SECONDS); +} + +static void dealer_performs_hits(void) { + give_card(&dealer); + display_dealer_hand(); + if (dealer.score > 21) { + display_win(); + } else if (dealer.score > player.score) { + display_lose(); + } else { + display_dealer_hand(); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); + } +} + +static void see_if_dealer_hits(void) { + if (dealer.score > 16) { + if (dealer.score > player.score) { + display_lose(); + } else if (dealer.score < player.score) { + display_win(); + } else { + display_tie(); + } + } else { + dealer_performs_hits(); + } +} + +static void handle_button_presses(bool tap_control_on, bool hit) { + switch (game_state) + { + case BJ_TITLE_SCREEN: + if (!tap_turned_on && tap_control_on) { + if (movement_enable_tap_detection_if_available()) tap_turned_on = true; + } + begin_playing(tap_control_on); + break; + case BJ_PLAYING: + if (hit) { + perform_hit(); + } else { + perform_stand(); + } + break; + case BJ_DEALER_PLAYING: + see_if_dealer_hits(); + break; + case BJ_BUST: + display_bust(); + break; + case BJ_RESULT: + case BJ_WIN_RATIO: + display_title(); + break; + } +} + +static void toggle_tap_control(blackjack_face_state_t *state) { + if (state->tap_control_on) { + movement_disable_tap_detection_if_available(); + state->tap_control_on = false; + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + } else { + bool tap_could_enable = movement_enable_tap_detection_if_available(); + if (tap_could_enable) { + state->tap_control_on = true; + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } + } +} + +void blackjack_face_setup(uint8_t watch_face_index, void **context_ptr) { + (void) watch_face_index; + + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(blackjack_face_state_t)); + memset(*context_ptr, 0, sizeof(blackjack_face_state_t)); + blackjack_face_state_t *state = (blackjack_face_state_t *)*context_ptr; + state->tap_control_on = false; + } + g_state = (blackjack_face_state_t *)*context_ptr; +} + +void blackjack_face_activate(void *context) { + blackjack_face_state_t *state = (blackjack_face_state_t *) context; + (void) state; + display_title(); + stack_deck(); +} + +bool blackjack_face_loop(movement_event_t event, void *context) { + blackjack_face_state_t *state = (blackjack_face_state_t *) context; + switch (event.event_type) { + case EVENT_ACTIVATE: + if (state->tap_control_on) watch_set_indicator(WATCH_INDICATOR_SIGNAL); + break; + case EVENT_TICK: + if (game_state == BJ_DEALER_PLAYING) { + see_if_dealer_hits(); + } + else if (game_state == BJ_BUST) { + display_bust(); + } + break; + case EVENT_LIGHT_BUTTON_UP: + case EVENT_DOUBLE_TAP: + handle_button_presses(state->tap_control_on, false); + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_ALARM_BUTTON_UP: + case EVENT_SINGLE_TAP: + handle_button_presses(state->tap_control_on, true); + break; + case EVENT_LIGHT_LONG_PRESS: + if (game_state == BJ_TITLE_SCREEN) { + display_win_ratio(state); + } else { + movement_illuminate_led(); + } + break; + case EVENT_ALARM_LONG_PRESS: + if (game_state == BJ_TITLE_SCREEN) { + toggle_tap_control(state); + } + break; + case EVENT_TIMEOUT: + case EVENT_LOW_ENERGY_UPDATE: + if (tap_turned_on) { + movement_disable_tap_detection_if_available(); + } + break; + default: + return movement_default_loop_handler(event); + } + return true; +} + +void blackjack_face_resign(void *context) { + (void) context; + if (tap_turned_on) { + tap_turned_on = false; + movement_disable_tap_detection_if_available(); + } +} diff --git a/watch-faces/complication/blackjack_face.h b/watch-faces/complication/blackjack_face.h new file mode 100755 index 00000000..c93143b9 --- /dev/null +++ b/watch-faces/complication/blackjack_face.h @@ -0,0 +1,91 @@ +/* + * 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 BLACKJACK_FACE_H_ +#define BLACKJACK_FACE_H_ + +#include "movement.h" + +/* + * Blackjack face + * ====================== + * + * Simple blackjack game. + * + * Aces are 11 unless you'd but, and if so, they become 1. + * King, Queen, and jack are all 10 points. + * Dealer deals to themselves until they get at least 17. + * The game plays with one shuffled deck that gets reshuffled with every game. + * + * Press either ALARM or LIGHT to begin playing. + * Your score is in the Seconds position. + * The dealer's score is in the Top-Right position. + * The dealer's last-shown card is in the Top-Left position. + * Your cards are in the Bottom row. From left to right, they are oldest to newest. Up to four cards will be dislayed. + * + * To hit, press the ALARM button. + * To stand, press the LIGHT button. + * If you're at 21, you cannoy hit, since we just assume it's a mispress on the button. + * + * Once you stand, the dealer will deal out to themselves once per second (or immidietly when you press the LIGHT or ALARM buttons). + * The game results are: + * WIN: You have a higher score than the dealer, but no more than 21. Or the dealer's score is over 21. + * LOSE: Your score is lower than the dealer's. + * BUST: Your score is above 21. + * TIE: Your score matches the dealer's final score + * + * On a watch that has the accelerometer, long-pressing the ALARM button will turn on the ability to play by tapping. + * The SIGNAL indicator will display when tapping is enabled. + * Tapping once will behave like the ALARM button and hit. + * Tapping twice behave like the LIGHT button and stand. + * + * | 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|-|=|≡|a| + * If you're using a custom display, Ace will display as 'A', not 'a' + */ + + + +typedef struct { + bool tap_control_on; + uint16_t games_played; + uint16_t games_won; +} blackjack_face_state_t; + +void blackjack_face_setup(uint8_t watch_face_index, void ** context_ptr); +void blackjack_face_activate(void *context); +bool blackjack_face_loop(movement_event_t event, void *context); +void blackjack_face_resign(void *context); + +#define blackjack_face ((const watch_face_t){ \ + blackjack_face_setup, \ + blackjack_face_activate, \ + blackjack_face_loop, \ + blackjack_face_resign, \ + NULL, \ +}) + +#endif // blackjack_FACE_H_ diff --git a/watch-faces/complication/counter_face.c b/watch-faces/complication/counter_face.c new file mode 100644 index 00000000..9db86fc3 --- /dev/null +++ b/watch-faces/complication/counter_face.c @@ -0,0 +1,153 @@ +/* + * MIT License + * + * Copyright (c) 2022 Shogo Okamoto + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include "counter_face.h" +#include "watch.h" +#include "watch_utility.h" +#include "watch_common_display.h" + +static inline bool lcd_is_custom(void) { + return watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; +} + +void counter_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(counter_state_t)); + memset(*context_ptr, 0, sizeof(counter_state_t)); + counter_state_t *state = (counter_state_t *)*context_ptr; + state->beep_on = true; + } +} + +void counter_face_activate(void *context) { + counter_state_t *state = (counter_state_t *)context; + if (state->beep_on) { + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } +} + +bool counter_face_loop(movement_event_t event, void *context) { + + counter_state_t *state = (counter_state_t *)context; + + switch (event.event_type) { + case EVENT_ALARM_BUTTON_UP: + watch_buzzer_abort_sequence(); //abort running buzzer sequence when counting fast + state->counter_idx++; // increment counter index + if (state->counter_idx>99) { //0-99 + state->counter_idx=0;//reset counter index + } + print_counter(state); + if (state->beep_on) { + beep_counter(state); + } + break; + case EVENT_LIGHT_LONG_PRESS: + watch_buzzer_abort_sequence(); + state->beep_on = !state->beep_on; + if (state->beep_on) { + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } else { + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + } + break; + case EVENT_ALARM_LONG_PRESS: + state->counter_idx=0; // reset counter index + print_counter(state); + break; + case EVENT_ACTIVATE: + print_counter(state); + break; + case EVENT_TIMEOUT: + // ignore timeout + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +// beep counter index times +void beep_counter(counter_state_t *state) { + int low_count = state->counter_idx/5; + int high_count = state->counter_idx - low_count * 5; + static int8_t sound_seq[15]; + memset(sound_seq, 0, 15); + int i = 0; + if (low_count > 0) { + sound_seq[i] = BUZZER_NOTE_A6; + i++; + sound_seq[i] = 3; + i++; + sound_seq[i] = BUZZER_NOTE_REST; + i++; + sound_seq[i] = 6; + i++; + if (low_count > 1) { + sound_seq[i] = -2; + i++; + sound_seq[i] = low_count-1; + i++; + } + sound_seq[i] = BUZZER_NOTE_REST; + i++; + sound_seq[i] = 6; + i++; + } + if (high_count > 0) { + sound_seq[i] = BUZZER_NOTE_B6; + i++; + sound_seq[i] = 3; + i++; + sound_seq[i] = BUZZER_NOTE_REST; + i++; + sound_seq[i] = 6; + i++; + } + if (high_count > 1) { + sound_seq[i] = -2; + i++; + sound_seq[i] = high_count-1; + } + watch_buzzer_play_sequence((int8_t *)sound_seq, NULL); +} + + +// print counter index at the center of display. +void print_counter(counter_state_t *state) { + char buf[14]; + watch_display_text_with_fallback(WATCH_POSITION_TOP, "COUNT", "CO"); + sprintf(buf, " %02d", state->counter_idx); // center of LCD display + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); + +} + +void counter_face_resign(void *context) { + (void) context; +} diff --git a/watch-faces/complication/counter_face.h b/watch-faces/complication/counter_face.h new file mode 100644 index 00000000..2ec68127 --- /dev/null +++ b/watch-faces/complication/counter_face.h @@ -0,0 +1,63 @@ +/* + * MIT License + * + * Copyright (c) 2022 Shogo Okamoto + * + * 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 COUNTER_FACE_H_ +#define COUNTER_FACE_H_ + +/* + * COUNTER face + * + * Counter face is designed to count the number of running laps during exercises. + * + * Usage: + * Short-press ALARM to increment the counter (loops at 99) + * Long-press ALARM to reset the counter. + * Long-press LIGHT to toggle sound. + */ + +#include "movement.h" + +typedef struct { + uint8_t counter_idx; + bool beep_on; +} counter_state_t; + + +void counter_face_setup(uint8_t watch_face_index, void ** context_ptr); +void counter_face_activate(void *context); +bool counter_face_loop(movement_event_t event, void *context); +void counter_face_resign(void *context); + +void print_counter(counter_state_t *state); +void beep_counter(counter_state_t *state); + +#define counter_face ((const watch_face_t){ \ + counter_face_setup, \ + counter_face_activate, \ + counter_face_loop, \ + counter_face_resign, \ + NULL, \ +}) + +#endif // COUNTER_FACE_H_ diff --git a/watch-faces/complication/deadline_face.c b/watch-faces/complication/deadline_face.c new file mode 100644 index 00000000..d3204f20 --- /dev/null +++ b/watch-faces/complication/deadline_face.c @@ -0,0 +1,637 @@ +/* + * MIT License + * + * Copyright (c) 2023-2025 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 +#include +#include "deadline_face.h" +#include "watch.h" +#include "watch_utility.h" + +/* Beep types */ +typedef enum { + BEEP_BUTTON, + BEEP_ENABLE, + BEEP_DISABLE +} beep_type_t; + +#define SETTINGS_NUM (5) +const char settings_titles[SETTINGS_NUM][6] = { "Year ", "Month", "Day ", "Hour ", "Minut" }; +const char settings_fallback_titles[SETTINGS_NUM][3] = { "YR", "MO", "DA", "HR", "M1" }; + +const char *running_title = "DUE"; +const char *running_fallback_title = "DL"; + +/* Local functions */ +static void _deadline_running_init(deadline_state_t * state); +static bool _deadline_running_loop(movement_event_t event, void *context); +static void _deadline_running_display(movement_event_t event, deadline_state_t * state); +static void _deadline_settings_init(deadline_state_t * state); +static bool _deadline_settings_loop(movement_event_t event, void *context); +static void _deadline_settings_display(movement_event_t event, deadline_state_t * state, + watch_date_time_t date); + +/* 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]; + } +} + +/* Play beep sound based on type */ +static inline void _beep(beep_type_t beep_type) +{ + if (!movement_button_should_sound()) + return; + + switch (beep_type) { + case BEEP_BUTTON: + watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume()); + break; + + case BEEP_ENABLE: + watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume()); + watch_buzzer_play_note(BUZZER_NOTE_REST, 75); + watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume()); + break; + + case BEEP_DISABLE: + watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume()); + watch_buzzer_play_note(BUZZER_NOTE_REST, 75); + watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume()); + break; + } +} + +/* 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(deadline_state_t *state) +{ + watch_date_time_t now = movement_get_local_date_time(); + uint32_t now_ts = watch_utility_date_time_to_unix_time(now, 0); + uint32_t min_ts = UINT32_MAX; + uint8_t min_index = 0; + + for (uint8_t i = 0; i < DEADLINE_FACE_DATES; i++) { + /* Skip expired deadlines and those further in the future than current minimum */ + 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(deadline_state_t *state) +{ + movement_play_alarm(); + movement_move_to_face(state->face_idx); +} + +/* Reset deadline to tomorrow */ +static inline void _reset_deadline(deadline_state_t *state) +{ + /* Get current time and reset hours/minutes/seconds */ + watch_date_time_t date_time = movement_get_local_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, 0); + ts += 24 * 60 * 60; + + state->deadlines[state->current_index] = ts; +} + +/* Calculate the naive difference between deadline and current time */ +static void _calculate_time_remaining(watch_date_time_t dl, watch_date_time_t now, int16_t *units) +{ + units[0] = dl.unit.second - now.unit.second; + units[1] = dl.unit.minute - now.unit.minute; + units[2] = dl.unit.hour - now.unit.hour; + units[3] = dl.unit.day - now.unit.day; + units[4] = dl.unit.month - now.unit.month; + units[5] = dl.unit.year - now.unit.year; +} + +/* Format the remaining time for display */ +static void _format_time_remaining(int16_t *units, char *buffer, size_t buffer_size) +{ + const int16_t years = units[5]; + const int16_t months = units[4]; + const int16_t days = units[3]; + const int16_t hours = units[2]; + const int16_t minutes = units[1]; + const int16_t seconds = units[0]; + + if (years > 0) { + /* years:months */ + snprintf(buffer, buffer_size, "%02d%02dYR", years % 100, months % 12); + } else if (months > 0) { + /* months:days */ + snprintf(buffer, buffer_size, "%02d%02dMO", (years * 12 + months) % 100, days % 32); + } else if (days > 0) { + /* days:hours */ + snprintf(buffer, buffer_size, "%02d%02ddY", days % 32, hours % 24); + } else { + /* hours:minutes:seconds */ + snprintf(buffer, buffer_size, "%02d%02d%02d", hours % 24, minutes % 60, seconds % 60); + } +} + +/* Correct the naive time difference calculation */ +static void _correct_time_difference(int16_t *units, watch_date_time_t deadline) +{ + const uint8_t range[] = { 60, 60, 24, 30, 12, 0 }; + + for (uint8_t i = 0; i < 6; i++) { + if (units[i] < 0) { + /* Correct remaining units */ + if (i == 3) { + units[i] += _days_in_month(deadline.unit.month - 1, deadline.unit.year); + } else { + units[i] += range[i]; + } + + /* Carry over change to next unit if non-zero */ + if (i < 5 && units[i + 1] != 0) { + units[i + 1] -= 1; + } + } + } +} + +/* Increment date in settings mode. Function taken from `set_time_face.c` */ +static void _increment_date(deadline_state_t *state, watch_date_time_t 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, 0); + state->deadlines[state->current_index] = ts; +} + +/* Update display in running mode */ +static void _deadline_running_display(movement_event_t event, deadline_state_t *state) +{ + (void) event; + + /* Seconds, minutes, hours, days, months, years */ + int16_t units[] = { 0, 0, 0, 0, 0, 0 }; + char buf[16]; + + /* Top row with face name and deadline index */ + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, running_title, running_fallback_title); + sprintf(buf, "%2d", state->current_index + 1); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + + /* Display indicators */ + if (state->alarm_enabled) + watch_set_indicator(WATCH_INDICATOR_BELL); + else + watch_clear_indicator(WATCH_INDICATOR_BELL); + + watch_date_time_t now = movement_get_local_date_time(); + uint32_t now_ts = watch_utility_date_time_to_unix_time(now, 0); + + /* Deadline expired */ + if (state->deadlines[state->current_index] < now_ts) { + if (state->deadlines[state->current_index] + 24 * 60 * 60 > now_ts) + sprintf(buf, "OVER "); + else + sprintf(buf, "---- "); + + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); + return; + } + + /* Get date time structs */ + uint32_t dl_ts = state->deadlines[state->current_index]; + watch_date_time_t deadline = watch_utility_date_time_from_unix_time(dl_ts, 0); + + /* Calculate and format time remaining */ + _calculate_time_remaining(deadline, now, units); + _correct_time_difference(units, deadline); + _format_time_remaining(units, buf, sizeof(buf)); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +/* Init running mode */ +static void _deadline_running_init(deadline_state_t *state) +{ + (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 _deadline_running_loop(movement_event_t event, void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + + if (event.event_type != EVENT_BACKGROUND_TASK) + _deadline_running_display(event, state); + + switch (event.event_type) { + case EVENT_ALARM_BUTTON_UP: + _beep(BEEP_BUTTON); + state->current_index = (state->current_index + 1) % DEADLINE_FACE_DATES; + _deadline_running_display(event, state); + break; + case EVENT_ALARM_LONG_PRESS: + _beep(BEEP_ENABLE); + _deadline_settings_init(state); + state->mode = DEADLINE_SETTINGS; + 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(BEEP_BUTTON); + state->alarm_enabled = !state->alarm_enabled; + _deadline_running_display(event, state); + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + case EVENT_BACKGROUND_TASK: + _background_alarm_play(state); + break; + case EVENT_LOW_ENERGY_UPDATE: + break; + default: + return movement_default_loop_handler(event); + } + + return true; +} + +/* Update display in settings mode */ +static void _deadline_settings_display(movement_event_t event, + deadline_state_t *state, watch_date_time_t date_time) +{ + char buf[7]; + + watch_display_text_with_fallback(WATCH_POSITION_TOP, settings_titles[state->current_page], + settings_fallback_titles[state->current_page]); + + if (state->current_page > 2) { + /* Time settings */ + watch_set_colon(); + if (movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_24H) { + /* 24h format */ + watch_set_indicator(WATCH_INDICATOR_24H); + sprintf(buf, "%2d%02d ", date_time.unit.hour, date_time.unit.minute); + } else { + /* 12h format */ + if (date_time.unit.hour < 12) + watch_clear_indicator(WATCH_INDICATOR_PM); + else + watch_set_indicator(WATCH_INDICATOR_PM); + uint8_t hour = date_time.unit.hour % 12; + sprintf(buf, "%2d%02d ", hour ? hour : 12, date_time.unit.minute); + } + } else { + /* Date settings */ + watch_clear_colon(); + watch_clear_indicator(WATCH_INDICATOR_24H); + watch_clear_indicator(WATCH_INDICATOR_PM); + sprintf(buf, "%2d%02d%02d", + 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[0] = buf[1] = ' '; + break; + case 1: + case 4: + buf[2] = buf[3] = ' '; + break; + case 2: + buf[4] = buf[5] = ' '; + break; + } + } + + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +/* Init setting mode */ +static void _deadline_settings_init(deadline_state_t *state) +{ + state->current_page = 0; + + /* Init fresh deadline to next day */ + if (state->deadlines[state->current_index] == 0) { + _reset_deadline(state); + } + + /* Ensure 1Hz updates only */ + _change_tick_freq(1, state); +} + +/* Loop of setting mode */ +static bool _deadline_settings_loop(movement_event_t event, void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + watch_date_time_t date_time; + date_time = watch_utility_date_time_from_unix_time(state->deadlines[state->current_index], 0); + + if (event.event_type != EVENT_BACKGROUND_TASK) + _deadline_settings_display(event, state, date_time); + + switch (event.event_type) { + case EVENT_TICK: + if (state->tick_freq == 8) { + if (HAL_GPIO_BTN_ALARM_read()) { + _increment_date(state, date_time); + _deadline_settings_display(event, 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(BEEP_BUTTON); + _reset_deadline(state); + break; + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_LIGHT_BUTTON_UP: + state->current_page = (state->current_page + 1) % SETTINGS_NUM; + _deadline_settings_display(event, state, date_time); + break; + case EVENT_ALARM_BUTTON_UP: + _change_tick_freq(4, state); + _increment_date(state, date_time); + _deadline_settings_display(event, state, date_time); + break; + case EVENT_TIMEOUT: + _beep(BEEP_BUTTON); + _change_tick_freq(1, state); + state->mode = DEADLINE_RUNNING; + movement_move_to_face(0); + break; + case EVENT_MODE_BUTTON_UP: + _beep(BEEP_DISABLE); + _deadline_running_init(state); + _deadline_running_display(event, state); + state->mode = DEADLINE_RUNNING; + break; + case EVENT_BACKGROUND_TASK: + _background_alarm_play(state); + break; + default: + return movement_default_loop_handler(event); + } + + return true; +} + +/* Setup face */ +void deadline_face_setup(uint8_t watch_face_index, void **context_ptr) +{ + (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(void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + + /* Set display options */ + _deadline_running_init(state); + state->mode = DEADLINE_RUNNING; + state->current_index = _closest_deadline(state); +} + +/* Loop face */ +bool deadline_face_loop(movement_event_t event, void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + switch (state->mode) { + case DEADLINE_SETTINGS: + _deadline_settings_loop(event, context); + break; + default: + case DEADLINE_RUNNING: + _deadline_running_loop(event, context); + break; + } + + return true; +} + +/* Resign face */ +void deadline_face_resign(void *context) +{ + (void) context; +} + + +/* Background task */ +movement_watch_face_advisory_t deadline_face_advise(void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + movement_watch_face_advisory_t retval = { 0 }; + + if (!state->alarm_enabled) + return retval; + + /* Determine closest deadline */ + watch_date_time_t now = movement_get_local_date_time(); + uint32_t now_ts = watch_utility_date_time_to_unix_time(now, 0); + uint32_t next_ts = state->deadlines[_closest_deadline(state)]; + + /* No active deadline */ + if (next_ts < now_ts) + return retval; + + /* No deadline within next 60 seconds */ + if (next_ts >= now_ts + 60) + return retval; + + /* Deadline within next minute. Let's set up an alarm */ + retval.wants_background_task = true; + return retval; +} diff --git a/watch-faces/complication/deadline_face.h b/watch-faces/complication/deadline_face.h new file mode 100644 index 00000000..98a50d2d --- /dev/null +++ b/watch-faces/complication/deadline_face.h @@ -0,0 +1,65 @@ +/* + * MIT License + * + * Copyright (c) 2023-2025 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_RUNNING = 0, + DEADLINE_SETTINGS +} 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(uint8_t watch_face_index, void **context_ptr); +void deadline_face_activate(void *context); +bool deadline_face_loop(movement_event_t event, void *context); +void deadline_face_resign(void *context); +movement_watch_face_advisory_t deadline_face_advise(void *context); + +#define deadline_face ((const watch_face_t){ \ + deadline_face_setup, \ + deadline_face_activate, \ + deadline_face_loop, \ + deadline_face_resign, \ + deadline_face_advise, \ +}) + +#endif // DEADLINE_FACE_H_ diff --git a/legacy/watch_faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c similarity index 69% rename from legacy/watch_faces/complication/endless_runner_face.c rename to watch-faces/complication/endless_runner_face.c index 1a173293..8a4b7a3b 100644 --- a/legacy/watch_faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -25,6 +25,7 @@ #include #include #include "endless_runner_face.h" +#include "delay.h" typedef enum { JUMPING_FINAL_FRAME = 0, @@ -34,6 +35,7 @@ typedef enum { typedef enum { SCREEN_TITLE = 0, + SCREEN_SCORE, SCREEN_PLAYING, SCREEN_LOSE, SCREEN_TIME, @@ -77,14 +79,45 @@ typedef struct { uint8_t fuel; } game_state_t; +// always-on, left, right, bottom, jump-top, jump-left, jump-right +int8_t classic_ball_arr_com[] = {1, 0, 1, 0, 2, 1, 2}; +int8_t classic_ball_arr_seg[] = {20, 20, 21, 21, 20, 17, 21}; +int8_t custom_ball_arr_com[] = {2, 1, 1, 0, 3, 3, 2}; +int8_t custom_ball_arr_seg[] = {15, 15, 14, 15, 14, 15, 14}; + +// obstacle 0-11 +int8_t classic_obstacle_arr_com[] = {0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1}; +int8_t classic_obstacle_arr_seg[] = {18, 19, 20, 21, 22, 23, 0, 1, 2, 4, 5, 6}; +int8_t custom_obstacle_arr_com[] = {1, 1, 1, 1, 1, 0, 1, 0, 3, 0, 0, 2}; +int8_t custom_obstacle_arr_seg[] = {22, 16, 15, 14, 1, 2, 3, 4, 4, 5, 6, 7}; + +int8_t *ball_arr_com; +int8_t *ball_arr_seg; +int8_t *obstacle_arr_com; +int8_t *obstacle_arr_seg; + static game_state_t game_state; static const uint8_t _num_bits_obst_pattern = sizeof(game_state.obst_pattern) * 8; +int8_t start_tune[] = { + BUZZER_NOTE_C5, 15, + BUZZER_NOTE_E5, 15, + BUZZER_NOTE_G5, 15, + 0 +}; + +int8_t lose_tune[] = { + BUZZER_NOTE_D3, 10, + BUZZER_NOTE_C3SHARP_D3FLAT, 10, + BUZZER_NOTE_C3, 10, + 0 +}; + 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); + printf("%u", (value >> i) & 1); // Optional: add a space every 4 bits for readability if (i % 4 == 0 && i != 0) { printf(" "); @@ -188,22 +221,22 @@ static uint32_t get_random_legal(uint32_t prev_val, uint16_t difficulty) { 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); + watch_set_pixel(ball_arr_com[3], ball_arr_seg[3]); + watch_set_pixel(ball_arr_com[2], ball_arr_seg[2]); + watch_set_pixel(ball_arr_com[1], ball_arr_seg[1]); + watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]); + watch_clear_pixel(ball_arr_com[6], ball_arr_seg[6]); + watch_clear_pixel(ball_arr_com[5], ball_arr_seg[5]); + watch_clear_pixel(ball_arr_com[4], ball_arr_seg[4]); } 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); + watch_clear_pixel(ball_arr_com[3], ball_arr_seg[3]); + watch_clear_pixel(ball_arr_com[2], ball_arr_seg[2]); + watch_clear_pixel(ball_arr_com[1], ball_arr_seg[1]); + watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]); + watch_set_pixel(ball_arr_com[6], ball_arr_seg[6]); + watch_set_pixel(ball_arr_com[5], ball_arr_seg[5]); + watch_set_pixel(ball_arr_com[4], ball_arr_seg[4]); } } @@ -212,12 +245,12 @@ static void display_score(uint8_t score) { if (game_state.fuel_mode) { score %= (MAX_DISP_SCORE_FUEL + 1); sprintf(buf, "%1d", score); - watch_display_string(buf, 0); + watch_display_text(WATCH_POSITION_TOP_LEFT, buf); } else { score %= (MAX_DISP_SCORE + 1); sprintf(buf, "%2d", score); - watch_display_string(buf, 2); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } } @@ -234,16 +267,16 @@ static void add_to_score(endless_runner_state_t *state) { 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. + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); // Blink the 0 fuel to show it cannot be refilled. return; } sprintf(buf, "%2d", game_state.fuel); - watch_display_string(buf, 2); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } 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_t date_time = watch_rtc_get_date_time(); + watch_date_time_t date_time = movement_get_local_date_time(); if ((state -> year_last_hi_score != date_time.unit.year) || (state -> month_last_hi_score != date_time.unit.month)) { @@ -255,28 +288,15 @@ static void check_and_reset_hi_score(endless_runner_state_t *state) { } 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; - } + static const char *labels[] = { + [DIFF_BABY] = " b", + [DIFF_EASY] = " E", + [DIFF_HARD] = " H", + [DIFF_FUEL] = " F", + [DIFF_FUEL_1] = "1F", + [DIFF_NORM] = " N" + }; + watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]); game_state.fuel_mode = difficulty >= DIFF_FUEL && difficulty <= DIFF_FUEL_1; } @@ -289,65 +309,93 @@ static void change_difficulty(endless_runner_state_t *state) { } } -static void toggle_sound(endless_runner_state_t *state) { - state -> soundOn = !state -> soundOn; - if (state -> soundOn){ - watch_buzzer_play_note(BUZZER_NOTE_C5, 30); +static void display_sound_indicator(bool soundOn) { + if (soundOn){ watch_set_indicator(WATCH_INDICATOR_BELL); - } - else { + } else { watch_clear_indicator(WATCH_INDICATOR_BELL); } } +static void toggle_sound(endless_runner_state_t *state) { + state -> soundOn = !state -> soundOn; + display_sound_indicator(state -> soundOn); + if (state -> soundOn){ + watch_buzzer_play_note(BUZZER_NOTE_C5, 30); + } +} + +static void enable_tap_control(endless_runner_state_t *state) { + if (!state->tap_control_on) { + movement_enable_tap_detection_if_available(); + state->tap_control_on = true; + } +} + +static void disable_tap_control(endless_runner_state_t *state) { + if (state->tap_control_on) { + movement_disable_tap_detection_if_available(); + state->tap_control_on = false; + } +} + static void display_title(endless_runner_state_t *state) { + game_state.curr_screen = SCREEN_TITLE; + watch_clear_colon(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "ENdLS", "ER "); + watch_display_text(WATCH_POSITION_BOTTOM, "RUNNER"); + display_sound_indicator(state -> soundOn); +} + +static void display_score_screen(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.curr_screen = SCREEN_SCORE; 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(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN ", "ER "); if (hi_score > MAX_HI_SCORE) { - watch_display_string("ER HS --", 0); + watch_display_text(WATCH_POSITION_BOTTOM, "HS --"); } else { - char buf[14]; - sprintf(buf, "ER HS%4d", hi_score); - watch_display_string(buf, 0); + char buf[10]; + sprintf(buf, "HS%4d", hi_score); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } display_difficulty(difficulty); + display_sound_indicator(sound_on); } -static void display_time(watch_date_time_t date_time, bool clock_mode_24h) { +static void display_time(void) { static watch_date_time_t previous_date_time; + watch_date_time_t date_time = movement_get_local_date_time(); + movement_clock_mode_t clock_mode_24h = movement_clock_mode_24h(); 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); + if (!watch_sleep_animation_is_running()) { + watch_set_colon(); + watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500); + } + if (clock_mode_24h != MOVEMENT_CLOCK_MODE_12H) 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); + sprintf( buf, clock_mode_24h == MOVEMENT_CLOCK_MODE_024H ? "%02d%02d " : "%2d%02d ", hour, date_time.unit.minute); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } - // 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); + // If only the minute need updating + else { + sprintf( buf, "%02d", date_time.unit.minute); + watch_display_text(WATCH_POSITION_MINUTES, buf); } previous_date_time.reg = date_time.reg; } @@ -356,36 +404,37 @@ static void begin_playing(endless_runner_state_t *state) { uint8_t difficulty = state -> difficulty; game_state.curr_screen = SCREEN_PLAYING; watch_clear_colon(); + display_sound_indicator(state -> soundOn); movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ); if (game_state.fuel_mode) { - watch_display_string(" ", 0); + watch_clear_display(); 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); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, " "); 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); + watch_buzzer_play_sequence(start_tune, NULL); } } 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 + watch_clear_display(); + watch_display_text(WATCH_POSITION_BOTTOM, " LOSE "); + if (state -> soundOn) { + watch_buzzer_play_sequence(lose_tune, NULL); delay_ms(600); + } } static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t *state) { @@ -395,9 +444,9 @@ static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t case 2: game_state.loc_2_on = obstacle; if (obstacle) - watch_set_pixel(0, 20); + watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); else if (game_state.jump_state != NOT_JUMPING) { - watch_clear_pixel(0, 20); + watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); if (game_state.fuel_mode && prev_obst_pos_two) add_to_score(state); } @@ -406,55 +455,20 @@ static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t case 3: game_state.loc_3_on = obstacle; if (obstacle) - watch_set_pixel(1, 21); + watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); else if (game_state.jump_state != NOT_JUMPING) - watch_clear_pixel(1, 21); + watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); 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: + if (obstacle) + watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); + else + watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); break; } } @@ -546,26 +560,37 @@ void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr) { memset(*context_ptr, 0, sizeof(endless_runner_state_t)); endless_runner_state_t *state = (endless_runner_state_t *)*context_ptr; state->difficulty = DIFF_NORM; + state->tap_control_on = false; } } void endless_runner_face_activate(void *context) { (void) context; + bool is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; + ball_arr_com = is_custom_lcd ? custom_ball_arr_com : classic_ball_arr_com; + ball_arr_seg = is_custom_lcd ? custom_ball_arr_seg : classic_ball_arr_seg; + obstacle_arr_com = is_custom_lcd ? custom_obstacle_arr_com : classic_obstacle_arr_com; + obstacle_arr_seg = is_custom_lcd ? custom_obstacle_arr_seg : classic_obstacle_arr_seg; + if (watch_sleep_animation_is_running()) { + watch_stop_blink(); + } } bool endless_runner_face_loop(movement_event_t event, void *context) { endless_runner_state_t *state = (endless_runner_state_t *)context; switch (event.event_type) { case EVENT_ACTIVATE: + disable_tap_control(state); 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_SCORE: case SCREEN_LOSE: + case SCREEN_TIME: break; default: update_game(state, event.subsecond); @@ -574,15 +599,37 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { 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); + switch (game_state.curr_screen) { + case SCREEN_SCORE: + enable_tap_control(state); + begin_playing(state); + break; + case SCREEN_TITLE: + enable_tap_control(state); + // fall through + case SCREEN_TIME: + case SCREEN_LOSE: + watch_clear_display(); + display_score_screen(state); + } break; case EVENT_LIGHT_LONG_PRESS: - if (game_state.curr_screen == SCREEN_TITLE) + if (game_state.curr_screen == SCREEN_SCORE) change_difficulty(state); break; + case EVENT_SINGLE_TAP: + case EVENT_DOUBLE_TAP: + if (state->difficulty > DIFF_HARD) break; // Don't do this on fuel modes + // Allow starting a new game by tapping. + if (game_state.curr_screen == SCREEN_SCORE) { + begin_playing(state); + break; + } + else if (game_state.curr_screen == SCREEN_LOSE) { + display_score_screen(state); + break; + } + //fall through case EVENT_LIGHT_BUTTON_DOWN: case EVENT_ALARM_BUTTON_DOWN: if (game_state.curr_screen == SCREEN_PLAYING && game_state.jump_state == NOT_JUMPING){ @@ -592,15 +639,21 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { } break; case EVENT_ALARM_LONG_PRESS: - if (game_state.curr_screen != SCREEN_PLAYING) + if (game_state.curr_screen == SCREEN_TITLE || game_state.curr_screen == SCREEN_SCORE) toggle_sound(state); break; case EVENT_TIMEOUT: - if (game_state.curr_screen != SCREEN_TITLE) - display_title(state); + disable_tap_control(state); + if (game_state.curr_screen != SCREEN_SCORE) + display_score_screen(state); break; case EVENT_LOW_ENERGY_UPDATE: - display_time(watch_rtc_get_date_time(), movement_clock_mode_24h()); + if (game_state.curr_screen != SCREEN_TIME) { + watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN ", "ER "); + display_sound_indicator(state -> soundOn); + display_difficulty(state->difficulty); + } + display_time(); break; default: return movement_default_loop_handler(event); @@ -609,6 +662,6 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { } void endless_runner_face_resign(void *context) { - (void) context; + endless_runner_state_t *state = (endless_runner_state_t *)context; + disable_tap_control(state); } - diff --git a/legacy/watch_faces/complication/endless_runner_face.h b/watch-faces/complication/endless_runner_face.h similarity index 91% rename from legacy/watch_faces/complication/endless_runner_face.h rename to watch-faces/complication/endless_runner_face.h index 3cfa6814..3a17a611 100644 --- a/legacy/watch_faces/complication/endless_runner_face.h +++ b/watch-faces/complication/endless_runner_face.h @@ -33,6 +33,8 @@ 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. + If the accelerometer is installed, you can tap the screen to jump and move through the menus after using the + buttons to go into the first game. High-score is displayed on the top-right on the title screen. During a game, the current score is displayed. */ @@ -42,7 +44,8 @@ typedef struct { 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 */ + uint8_t tap_control_on : 1; + uint8_t unused : 7; } endless_runner_state_t; void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr); diff --git a/legacy/watch_faces/complication/higher_lower_game_face.c b/watch-faces/complication/higher_lower_game_face.c similarity index 81% rename from legacy/watch_faces/complication/higher_lower_game_face.c rename to watch-faces/complication/higher_lower_game_face.c index 3491b5bd..d47e1f61 100755 --- a/legacy/watch_faces/complication/higher_lower_game_face.c +++ b/watch-faces/complication/higher_lower_game_face.c @@ -30,19 +30,22 @@ #include #include #include "higher_lower_game_face.h" -#include "watch_private_display.h" +#include "watch_common_display.h" + + +#define KING 12 +#define QUEEN 11 +#define JACK 10 #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 MAX_CARD_VALUE KING #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) @@ -111,7 +114,6 @@ static void shuffle_deck(void) { static void reset_deck(void) { current_card = 0; - stack_deck(); shuffle_deck(); } @@ -141,8 +143,8 @@ static void reset_board(bool first_round) { static void init_game(void) { watch_clear_display(); - watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START); - watch_display_string("GA", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT); + watch_display_text(WATCH_POSITION_TOP_LEFT, "HL"); reset_deck(); reset_board(true); score = 0; @@ -151,16 +153,23 @@ static void init_game(void) { } 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; + digit_mapping_t segmap; + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) { + segmap = Custom_LCD_Display_Mapping[position]; + } else { + segmap = Classic_LCD_Display_Mapping[position]; + } + const uint8_t com_pin = segmap.segment[segment].address.com; + const uint8_t seg = segmap.segment[segment].address.seg; watch_set_pixel(com_pin, seg); } +static inline size_t get_display_position(size_t board_position) { + return FLIP_BOARD_DIRECTION ? BOARD_DISPLAY_START + board_position : BOARD_DISPLAY_END - board_position; +} + 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 size_t display_position = get_display_position(board_position); const bool revealed = game_board[board_position].revealed; //// Current position indicator spot @@ -178,18 +187,18 @@ static void render_board_position(size_t board_position) { const uint8_t value = game_board[board_position].value; switch (value) { - case 14: // A (≡) + case KING: // K (≡) 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 (=) + case QUEEN: // Q (=) watch_display_character(' ', display_position); set_segment_at_position(A, display_position); set_segment_at_position(D, display_position); break; - case 12: // Q (-) + case JACK: // J (-) watch_display_character('-', display_position); break; default: { @@ -209,16 +218,16 @@ 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); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } static void render_final_score(void) { - watch_display_string("SC", STATUS_DISPLAY_START); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "SCORE", "SC "); 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); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } static guess_t get_answer(void) { @@ -251,13 +260,13 @@ static void do_game_loop(guess_t user_guess) { // Render answer indicator switch (answer) { case HL_GUESS_EQUAL: - watch_display_string("==", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_TOP_LEFT, "=="); break; case HL_GUESS_HIGHER: - watch_display_string("HI", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_TOP_LEFT, "HI"); break; case HL_GUESS_LOWER: - watch_display_string("LO", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_TOP_LEFT, "LO"); break; } @@ -268,18 +277,22 @@ static void do_game_loop(guess_t user_guess) { // No score for two consecutive identical cards } else { // Incorrect guess, game over - watch_display_string("GO", STATUS_DISPLAY_START); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "End", "GO"); game_board[guess_position].revealed = true; + watch_display_text(WATCH_POSITION_BOTTOM, "------"); + render_board_position(guess_position - 1); render_board_position(guess_position); + if (game_board[guess_position].value == JACK && guess_position < GAME_BOARD_SIZE) // Adds a space in case the revealed option is '-' + watch_display_character(' ', get_display_position(guess_position + 1)); 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); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "WI"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WINNER", "winnEr"); game_state = HL_GS_WIN; return; } @@ -309,12 +322,12 @@ static void do_game_loop(guess_t user_guess) { break; case HL_GS_SHOW_SCORE: watch_clear_display(); - watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START); - watch_display_string("GA", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT); + watch_display_text(WATCH_POSITION_TOP_LEFT, "HL"); game_state = HL_GS_TITLE_SCREEN; break; default: - watch_display_string("ERROR", BOARD_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, "ERROR"); break; } } @@ -344,6 +357,7 @@ void higher_lower_game_face_activate(void *context) { (void) state; // Handle any tasks related to your watch face coming on screen. game_state = HL_GS_TITLE_SCREEN; + stack_deck(); } bool higher_lower_game_face_loop(movement_event_t event, void *context) { @@ -353,8 +367,8 @@ bool higher_lower_game_face_loop(movement_event_t event, void *context) { 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); + watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT); + watch_display_text(WATCH_POSITION_TOP_LEFT, "HL"); break; case EVENT_TICK: // If needed, update your display here. diff --git a/legacy/watch_faces/complication/higher_lower_game_face.h b/watch-faces/complication/higher_lower_game_face.h similarity index 100% rename from legacy/watch_faces/complication/higher_lower_game_face.h rename to watch-faces/complication/higher_lower_game_face.h diff --git a/legacy/watch_faces/complication/interval_face.c b/watch-faces/complication/interval_face.c similarity index 82% rename from legacy/watch_faces/complication/interval_face.c rename to watch-faces/complication/interval_face.c index c354e722..15593567 100644 --- a/legacy/watch_faces/complication/interval_face.c +++ b/watch-faces/complication/interval_face.c @@ -28,8 +28,6 @@ #include "interval_face.h" #include "watch.h" #include "watch_utility.h" -#include "watch_private_display.h" -#include "watch_buzzer.h" typedef enum { interval_setting_0_timer_idx, @@ -48,10 +46,15 @@ typedef enum { } interval_setting_idx_t; #define INTERVAL_FACE_STATE_DEFAULT "IT" // Interval Timer +#define INTERVAL_FACE_STATE_DEFAULT_CD "INT" #define INTERVAL_FACE_STATE_WARMUP "PR" // PRepare / warm up +#define INTERVAL_FACE_STATE_WARMUP_CD "PRE" #define INTERVAL_FACE_STATE_WORK "WO" // WOrk +#define INTERVAL_FACE_STATE_WORK_CD "WOR" #define INTERVAL_FACE_STATE_BREAK "BR" // BReak +#define INTERVAL_FACE_STATE_BREAK_CD "BRK" #define INTERVAL_FACE_STATE_COOLDOWN "CD" // CoolDown +#define INTERVAL_FACE_STATE_COOLDOWN_CD "CLD" // Define some default timer settings. Each timer is described in an array like this: // 1. warm-up seconds, @@ -68,7 +71,7 @@ static const int8_t _default_timers[6][5] = {{0, 40, 20, 0, 0}, {0, -20, -5, 0, 0}}; static const uint8_t _intro_segdata[4][2] = {{1, 8}, {0, 8}, {0, 7}, {1, 7}}; -static const uint8_t _blink_idx[] = {3, 9, 4, 6, 4, 6, 8, 4, 6, 8, 4, 6}; +static const uint8_t _intro_segdata_cd[4][2] = {{1, 8}, {1, 9}, {0, 9}, {0, 8}}; static const uint8_t _setting_page_idx[] = {1, 0, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4}; static const int8_t _sound_seq_warmup[] = {BUZZER_NOTE_F6, 8, BUZZER_NOTE_REST, 1, -2, 3, 0}; static const int8_t _sound_seq_work[] = {BUZZER_NOTE_F6, 8, BUZZER_NOTE_REST, 1, -2, 2, BUZZER_NOTE_C7, 24, 0}; @@ -102,41 +105,56 @@ static inline void _button_beep() { if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_C7, 50); } -static void _timer_write_info(interval_face_state_t *state, char *buf, uint8_t timer_page) { +static void _timer_write_info(interval_face_state_t *state, char* bottom_row, char state_str[2][4], char* index_str, uint8_t timer_page) { // fill display string with requested timer information switch (timer_page) { case 0: // clear timer? - sprintf(buf, "%2s %1dCLEARn", INTERVAL_FACE_STATE_DEFAULT, state->timer_idx + 1); - if (_erase_timer_flag) buf[9] = 'y'; + sprintf(bottom_row, "CLEARn"); + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_DEFAULT); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_DEFAULT_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); + if (_erase_timer_flag) bottom_row[5] = 'y'; watch_clear_colon(); break; case 1: // warmup time info - sprintf(buf, "%2s %1d%02d%02d ", INTERVAL_FACE_STATE_WARMUP, state->timer_idx + 1, + sprintf(bottom_row, "%02d%02d ", state->timer[state->timer_idx].warmup_minutes, state->timer[state->timer_idx].warmup_seconds); + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_WARMUP); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_WARMUP_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); break; case 2: // work interval info - sprintf(buf, "%2s %1d%02d%02d%2d", INTERVAL_FACE_STATE_WORK, state->timer_idx + 1, + sprintf(bottom_row, "%02d%02d%2d", state->timer[state->timer_idx].work_minutes, state->timer[state->timer_idx].work_seconds, state->timer[state->timer_idx].work_rounds); + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_WORK); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_WORK_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); break; case 3: // break interval info - sprintf(buf, "%2s %1d%02d%02d%2d", INTERVAL_FACE_STATE_BREAK, state->timer_idx + 1, + sprintf(bottom_row, "%02d%02d%2d", state->timer[state->timer_idx].break_minutes, state->timer[state->timer_idx].break_seconds, state->timer[state->timer_idx].full_rounds); - if (!state->timer[state->timer_idx].full_rounds) buf[9] = '-'; + if (!state->timer[state->timer_idx].full_rounds) bottom_row[5] = '-'; + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_BREAK); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_BREAK_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); break; case 4: // cooldown time info - sprintf(buf, "%2s %1d%02d%02d ", INTERVAL_FACE_STATE_COOLDOWN ,state->timer_idx + 1, + sprintf(bottom_row, "%02d%02d ", state->timer[state->timer_idx].cooldown_minutes, state->timer[state->timer_idx].cooldown_seconds); + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_COOLDOWN); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_COOLDOWN_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); break; default: break; @@ -146,8 +164,10 @@ static void _timer_write_info(interval_face_state_t *state, char *buf, uint8_t t static void _face_draw(interval_face_state_t *state, uint8_t subsecond) { // draws current face state if (!state->is_active) return; - char buf[14]; - buf[0] = 0; + char bottom_row[10]; + char int_state_str[2][4]; + char int_index_str[5]; + bottom_row[0] = 0; uint8_t tmp; if (state->face_state == interval_state_waiting && _ticks >= 0) { // play info slideshow for current timer @@ -160,9 +180,9 @@ static void _face_draw(interval_face_state_t *state, uint8_t subsecond) { } } tmp = ticks / 3 + 1; - _timer_write_info(state, buf, tmp); + _timer_write_info(state, bottom_row, int_state_str, int_index_str, tmp); // don't show '1 round' when displaying workout time to avoid detail overload - if (tmp == 2 && state->timer[state->timer_idx].work_rounds == 1) buf[9] = ' '; + if (tmp == 2 && state->timer[state->timer_idx].work_rounds == 1) bottom_row[5] = ' '; // blink colon if (subsecond % 2 == 0 && _ticks < 24) watch_clear_colon(); else watch_set_colon(); @@ -175,38 +195,66 @@ static void _face_draw(interval_face_state_t *state, uint8_t subsecond) { } else { tmp = _setting_page_idx[_setting_idx]; } - _timer_write_info(state, buf, tmp); + _timer_write_info(state, bottom_row, int_state_str, int_index_str, tmp); // blink at cursor position if (subsecond % 2 && _ticks != -2) { - buf[_blink_idx[_setting_idx]] = ' '; - if (_blink_idx[_setting_idx] % 2 == 0) buf[_blink_idx[_setting_idx] + 1] = ' '; + switch (_setting_idx) { + case interval_setting_0_timer_idx: + int_index_str[0] = int_index_str[1] = ' '; + break; + case interval_setting_1_clear_yn: + bottom_row[5] = ' '; + break; + case interval_setting_2_warmup_minutes: + case interval_setting_4_work_minutes: + case interval_setting_7_break_minutes: + case interval_setting_10_cooldown_minutes: + bottom_row[0] = bottom_row[1] = ' '; + break; + case interval_setting_3_warmup_seconds: + case interval_setting_5_work_seconds: + case interval_setting_8_break_seconds: + case interval_setting_11_cooldown_seconds: + bottom_row[2] = bottom_row[3] = ' '; + break; + case interval_setting_6_work_rounds: + case interval_setting_9_full_rounds: + bottom_row[4] = bottom_row[5] = ' '; + break; + default: + break; + } } // show lap indicator only when rounds are set if (_setting_idx == interval_setting_6_work_rounds || _setting_idx == interval_setting_9_full_rounds) watch_set_indicator(WATCH_INDICATOR_LAP); - else + else watch_clear_indicator(WATCH_INDICATOR_LAP); } else if (state->face_state == interval_state_running || state->face_state == interval_state_pausing) { tmp = _timer_full_round; switch (_timer_run_state) { case 0: - sprintf(buf, INTERVAL_FACE_STATE_WARMUP); + sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_WARMUP); + sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_WARMUP_CD); break; case 1: - sprintf(buf, INTERVAL_FACE_STATE_WORK); + sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_WORK); + sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_WORK_CD); if (state->timer[state->timer_idx].work_rounds > 1) tmp = _timer_work_round; break; case 2: - sprintf(buf, INTERVAL_FACE_STATE_BREAK); + sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_BREAK); + sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_BREAK_CD); break; case 3: - sprintf(buf, INTERVAL_FACE_STATE_COOLDOWN); + sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_COOLDOWN); + sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_COOLDOWN_CD); break; default: break; } div_t delta; - + if (state->face_state == interval_state_pausing) { // pausing delta = div(_target_ts - _paused_ts, 60); @@ -216,16 +264,21 @@ static void _face_draw(interval_face_state_t *state, uint8_t subsecond) { } else // running delta = div(_target_ts - _now_ts, 60); - sprintf(&buf[2], " %1d%02d%02d%2d", state->timer_idx + 1, delta.quot, delta.rem, tmp + 1); + sprintf(bottom_row, "%02d%02d%2d", delta.quot, delta.rem, tmp + 1); + sprintf(int_index_str, " %1d", state->timer_idx + 1); } // write out to lcd - if (buf[0]) { - watch_display_character(buf[0], 0); - watch_display_character(buf[1], 1); + if (int_state_str[0][0]) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, int_state_str[1], int_state_str[0]); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, int_index_str, int_index_str); // set the bar for the i-like symbol on position 2 - watch_set_pixel(2, 9); + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { + watch_set_pixel(2, 9); + } else { + watch_set_pixel(2, 10); + } // display the rest of the string - watch_display_string(&buf[3], 3); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, bottom_row, bottom_row); } } @@ -427,8 +480,12 @@ bool interval_face_loop(movement_event_t event, void *context) { _init_timer_info(state); _face_draw(state, event.subsecond); break; - } - watch_set_pixel(_intro_segdata[_ticks][0], _intro_segdata[_ticks][1]); + } + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { + watch_set_pixel(_intro_segdata[_ticks][0], _intro_segdata[_ticks][1]); + } else { + watch_set_pixel(_intro_segdata_cd[_ticks][0], _intro_segdata_cd[_ticks][1]); + } _ticks++; } else if (state->face_state == interval_state_waiting && _ticks >= 0) { // play information slideshow for current interval timer @@ -448,7 +505,7 @@ bool interval_face_loop(movement_event_t event, void *context) { } break; case EVENT_ACTIVATE: - watch_display_string(INTERVAL_FACE_STATE_DEFAULT, 0); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, INTERVAL_FACE_STATE_DEFAULT_CD, INTERVAL_FACE_STATE_DEFAULT); if (state->face_state) _face_draw(state, event.subsecond); break; case EVENT_LIGHT_BUTTON_UP: @@ -479,7 +536,7 @@ bool interval_face_loop(movement_event_t event, void *context) { } break; case EVENT_LIGHT_LONG_PRESS: - _button_beep(settings); + _button_beep(); if (state->face_state == interval_state_setting) { _resume_setting(state, event.subsecond); } else { @@ -490,8 +547,10 @@ bool interval_face_loop(movement_event_t event, void *context) { case EVENT_ALARM_BUTTON_UP: switch (state->face_state) { case interval_state_waiting: - // cycle through timers - _inc_uint8(&state->timer_idx, 1, INTERVAL_TIMERS); + // cycle through timers, skipping empty ones + do { + _inc_uint8(&state->timer_idx, 1, INTERVAL_TIMERS); + } while (_is_timer_empty(&state->timer[state->timer_idx]) && state->timer_idx != 0); _ticks = 0; _face_draw(state, event.subsecond); break; @@ -502,7 +561,7 @@ bool interval_face_loop(movement_event_t event, void *context) { break; case interval_state_running: // pause timer - _button_beep(settings); + _button_beep(); _paused_ts = _get_now_ts(); state->face_state = interval_state_pausing; movement_cancel_background_task(); @@ -510,7 +569,7 @@ bool interval_face_loop(movement_event_t event, void *context) { break; case interval_state_pausing: // resume paused timer - _button_beep(settings); + _button_beep(); _resume_paused_timer(state); _face_draw(state, event.subsecond); break; @@ -527,7 +586,7 @@ bool interval_face_loop(movement_event_t event, void *context) { } else if (state->face_state <= interval_state_waiting) { if (_is_timer_empty(timer)) { // jump back to timer #1 - _button_beep(settings); + _button_beep(); state->timer_idx = 0; _init_timer_info(state); } else { @@ -551,7 +610,7 @@ bool interval_face_loop(movement_event_t event, void *context) { _init_timer_info(state); } else if (state->face_state == interval_state_pausing) { // resume paused timer - _button_beep(settings); + _button_beep(); _resume_paused_timer(state); } _face_draw(state, event.subsecond); diff --git a/legacy/watch_faces/complication/interval_face.h b/watch-faces/complication/interval_face.h similarity index 100% rename from legacy/watch_faces/complication/interval_face.h rename to watch-faces/complication/interval_face.h diff --git a/watch-faces/complication/lander_face.c b/watch-faces/complication/lander_face.c new file mode 100644 index 00000000..8381ff0a --- /dev/null +++ b/watch-faces/complication/lander_face.c @@ -0,0 +1,577 @@ +/* + * MIT License + * + * Copyright (c) 2024 Klingon Jane + * + * 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 +#endif + +#include +#include +#include +#include "lander_face.h" +#include "watch_common_display.h" + +#ifndef max +#define max(x, y) ((y) > (x) ? (y) : (x)) +#endif + +#ifndef min +#define min(x, y) ((x) > (y) ? (y) : (x)) +#endif + +#define LANDER_TICK_FREQUENCY 8 +#define MONSTER_DISPLAY_TICKS 9 +#define ENGINE_THRUST 11 +#define MODE_WAITING_TO_START 0 +#define MODE_DISPLAY_SKILL_LEVEL 1 +#define MODE_PLAYING 2 +#define MODE_TOUCHDOWN_BLANK 3 +#define MODE_DISPLAY_FINAL_STATUS 4 +#define MODE_MONSTER 5 +#define MODE_FIND_EARTH_MESSAGE 6 +#define CREWS_COMPLIMENT 13 +// Granularity is divisions per foot - height display +#define GRANUL 40 +// Next lines for repeat heroes only. +#define PROMOTION_INTERVAL 3 +#define LEVEL_ACE 8 +#define LEVEL_STARBUCK 11 +#define HARD_EARTH_INCREMENTS 11 +#define MAX_HARD_EARTH_CHANCE 6 + +// The gory final result calculations: +#define SPEED_FATALITY_ALL 41 +#define SPEED_FATALITY_NONE 26 +#define SPEED_NO_DAMAGE 21 +#define SPEED_LEVEL_INCREMENTS 2 +#define SPEED_MAJOR_CRASH 73 +#define MAJOR_CRASH_INCREMENTS 65 +#define SPEED_INJURY_NONE 20 +#define SPEED_INJURY_FULCRUM 32 +#define INJURY_FULCRUM_PROB 65 +#define FUEL_SCORE_GOOD 145 +#define FUEL_SCORE_GREAT 131 +#define FUEL_SCORE_FANTASTIC 125 + +// Joey Castillo to oversee storage allocation row +#define LANDER_STORAGE_ROW 2 +#define STORAGE_KEY_NUMBER 110 + +#define DIFFICULTY_LEVELS 3 +char lander_difficulty_names[DIFFICULTY_LEVELS][7] = { + "NOrMAL", "HArd ", "HArdEr" +}; +#define MONSTER_TYPES 4 +char lander_monster_names[MONSTER_TYPES][7] = { + "mOnStr", "6Erbil", "HAmStr", "Rabbit" +}; +#define MONSTER_ACTIONS 8 +char lander_monster_actions[MONSTER_ACTIONS][7] = { + "HUn6ry", " EAtS", "6Reedy", "annoYd", "nASty ", "SAVOry", "HO66SH", " pI66Y" +}; + +// -------------- +// Custom methods +// -------------- + + +static int gen_random_int (int16_t lower, int16_t upper) { + int range; + int retVal; + range = upper - lower + 1; + if ( range < 2 ) range = 2; + // Emulator: use rand. Hardware: use arc4random. + #if __EMSCRIPTEN__ + retVal = rand() % range; + #else + retVal = arc4random_uniform(range); + #endif + retVal += lower; + return retVal; +} + +static uint8_t assignProb ( uint8_t lowerProb, uint8_t upperProb, int16_t lowerSpeed, int16_t upperSpeed, int16_t actSpeed ) { + float probRange, speedRange; + float ratio, probFloat; + int probInt; + speedRange = upperSpeed - lowerSpeed; + if (speedRange<1.0) speedRange = 1.0; + probRange = upperProb - lowerProb; + ratio = ( (float) actSpeed - (float) lowerSpeed ) / speedRange; + probFloat = (float) lowerProb + ( ratio * probRange ); + probInt = (int) ( probFloat + 0.5 ); + probInt = min ( probInt, upperProb ); + probInt = max ( probInt, lowerProb ); + return (uint8_t) probInt; +} + +static void write_to_lander_EEPROM(lander_state_t *state) { + uint8_t output_array [ 3 ]; + output_array [ 0 ] = STORAGE_KEY_NUMBER; + output_array [ 1 ] = state->hero_counter; + output_array [ 2 ] = state->legend_counter; + watch_storage_erase ( LANDER_STORAGE_ROW ); + watch_storage_sync ( ); + watch_storage_write ( LANDER_STORAGE_ROW, 0, output_array, 3 ); +} + +// --------------------------- +// Standard watch face methods +// --------------------------- +void lander_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(lander_state_t)); + memset(*context_ptr, 0, sizeof(lander_state_t)); + lander_state_t *state = (lander_state_t *)*context_ptr; + state->led_enabled = false; + } + // Emulator only: Seed random number generator + #if __EMSCRIPTEN__ + srand(time(NULL)); + #endif +} + +void lander_face_activate(void *context) { + lander_state_t *state = (lander_state_t *)context; + char buf [ 7 ]; + state->mode = MODE_WAITING_TO_START; + state->led_active = false; + state->reset_counter = 0; + watch_clear_all_indicators ( ); + uint32_t offset = 0; + uint32_t size = 3; + uint8_t stored_data [ size ]; + // See if the hero_counter was ever written to EEPROM storage + watch_storage_read (LANDER_STORAGE_ROW, offset, stored_data, size); + if (stored_data[0] == STORAGE_KEY_NUMBER ) + { + state->hero_counter = stored_data [1]; // There's real data in there. + state->legend_counter = stored_data [2]; + } + else + { + state->hero_counter = 0; // Nope. Nothing there. + state->legend_counter = 0; + write_to_lander_EEPROM(state); // Initial EEPROM tracking data. + } + state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL; + state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit + // Fancy intro + if ( state->legend_counter == 0 ) watch_display_text(WATCH_POSITION_TOP_LEFT, "LA"); + else watch_display_text(WATCH_POSITION_TOP_LEFT, "LE"); + if ( ( state->hero_counter == 0 ) || ( state->hero_counter >= 40 ) ) watch_display_text ( WATCH_POSITION_TOP_RIGHT, " "); + else + { + sprintf ( buf, "%2d", state->hero_counter ); + watch_display_text ( WATCH_POSITION_TOP_RIGHT, buf); + } + if ( state->hero_counter >= 100 ) sprintf ( buf, "Str%3d", state->hero_counter ); + else if ( state->hero_counter >= 40 ) sprintf ( buf, "Strb%2d", state->hero_counter ); + else if ( state->hero_counter >= LEVEL_STARBUCK ) sprintf ( buf, "StrbUC" ); + else if ( state->hero_counter >= LEVEL_ACE ) sprintf ( buf, " ACE " ); // This human is good + else if ( state->difficulty_level == 0 ) sprintf ( buf, " " ); + else sprintf ( buf, "%s", lander_difficulty_names[state->difficulty_level] ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf); + if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL); + else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); +} + +bool lander_face_loop(movement_event_t event, void *context) { + lander_state_t *state = (lander_state_t *)context; + char buf [ 20 ]; // [11] is more correct and works; compiler too helpful. + + switch (event.event_type) { + case EVENT_TICK: + state->tick_counter++; + if ( state->mode == MODE_PLAYING ) { + int16_t accel = state->gravity; + bool gas_pedal_on = HAL_GPIO_BTN_ALARM_read() || HAL_GPIO_BTN_LIGHT_read(); + if ( gas_pedal_on && ( state->fuel_remaining > 0 ) ) { + accel = ENGINE_THRUST + state->gravity; // Gravity is negative + state->fuel_remaining--; // Used 1 fuel unit + watch_set_indicator ( WATCH_INDICATOR_LAP ); + // Low fuel warning indicators + if ( state->fuel_remaining == ( 3 * LANDER_TICK_FREQUENCY ) ) { // 3 seconds of fuel left + watch_set_indicator ( WATCH_INDICATOR_SIGNAL ); + watch_set_indicator ( WATCH_INDICATOR_BELL ); + watch_set_indicator ( WATCH_INDICATOR_PM ); + watch_set_indicator ( WATCH_INDICATOR_24H ); + } + else if ( state->fuel_remaining == 0 ) { // 0 seconds of fuel left, empty! + watch_clear_all_indicators ( ); + } + } + else { + watch_clear_indicator ( WATCH_INDICATOR_LAP ); + } + state->speed += accel; + state->height += state->speed; + if ( state->height > 971 * 80 ) { // Escape height + watch_clear_all_indicators (); + watch_display_text( WATCH_POSITION_BOTTOM, "ESCAPE" ); + state->tick_counter = 0; + state->mode = MODE_WAITING_TO_START; + } + else if ( state->height <= 0 ) { // Touchdown + state->tick_counter = 0; + state->mode = MODE_TOUCHDOWN_BLANK; + } + else { + // Update height display + sprintf ( buf, "%4d", (int) ( state->height / GRANUL ) ); + watch_display_text( WATCH_POSITION_BOTTOM, buf ); + } + } + else if ( state->mode == MODE_TOUCHDOWN_BLANK ) { + // Blank display on touchdown + if ( state->tick_counter == 1 ) { + watch_clear_all_indicators (); + watch_display_text( WATCH_POSITION_BOTTOM, " " ); + + // Also calc fuel score now. + float fuel_score_float; + uint16_t fuel_used; + fuel_used = state->fuel_start - state->fuel_remaining; + fuel_score_float = (float) fuel_used / (float) state->fuel_tpl; + state->fuel_score = (int) (fuel_score_float * 100.0 + 0.5); + if ( state->legend_counter == 0 ) state->fuel_score -= 8; // First Earth is easier + // Monitor reset_counter + if ( fuel_used >= 1 ) state->reset_counter = 0; + else state->reset_counter++; + if ( state->reset_counter >= 3 ) { + state->hero_counter = 0; + state->difficulty_level = 0; + if ( state->reset_counter >= 6 ) state->legend_counter = 0; + watch_display_text(WATCH_POSITION_BOTTOM, "rESET "); + write_to_lander_EEPROM(state); + } + } + // Wait until time for next display + if ( state->tick_counter >= ( 1 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + state->mode = MODE_DISPLAY_FINAL_STATUS; + } + } + else if ( state->mode == MODE_DISPLAY_FINAL_STATUS ) { + bool last_pass = false; + if ( state->tick_counter >= LANDER_TICK_FREQUENCY ) last_pass = true; + + // Show final status + if ( state->tick_counter == 1 ) { + // Calculate many attributes + // 1) Major crash: bug, crater, vaporized (gone). + // 2) Rank ship's health 0 to 8 + // 3) Crew fatalities and injuries + // 4) Special conditions: hero + // 5) Set fuel conservation indicators as appropriate + // 6) Set coffee maker OK indicator as appropriate + // 7) Green light if ship intact + // 8) Set standard display if not preempted. + bool allDone; + int16_t finalSpeed, boostedSpeed, levelsDamage; + int8_t shipsHealth, myRand; + uint8_t fatalities, probFatal, probInjury; + uint8_t i; + + allDone = false; + // Easiest implementation for difficulty_level is to increase touchdown speed above actual. + finalSpeed = abs ( state->speed ) + state->difficulty_level * 4; + // First Earth is a bit easier than all the others + if ( state->legend_counter == 0 ) finalSpeed -= 2; + + // 1) Major crash: bug, crater, vaporized (gone). + if ( finalSpeed >= SPEED_MAJOR_CRASH ) { + allDone = true; + shipsHealth = -1; + if ( finalSpeed >= ( SPEED_MAJOR_CRASH + 2 * MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, "GOnE " ); + else if ( finalSpeed >= ( SPEED_MAJOR_CRASH + MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, " CrAtr" ); + else sprintf ( buf, " bU6" ); + } + // 2) Rank ship's health 0 to 8 + if (!allDone) { + boostedSpeed = finalSpeed + SPEED_LEVEL_INCREMENTS - 1; + levelsDamage = (int) ( ( boostedSpeed - SPEED_NO_DAMAGE ) / SPEED_LEVEL_INCREMENTS ); + shipsHealth = 8 - levelsDamage; + shipsHealth = min ( shipsHealth, 8 ); // Keep between 0 and 8 + shipsHealth = max ( shipsHealth, 0 ); + } + state->ships_health = shipsHealth; // Remember ships health + // 3) Crew fatalities and injuries + if (!allDone) { + // Fatalies + probFatal = assignProb ( 0, 92, SPEED_FATALITY_NONE, SPEED_FATALITY_ALL, finalSpeed ); + // Injuries + if ( finalSpeed <= SPEED_INJURY_FULCRUM ) { + probInjury = assignProb ( 0, INJURY_FULCRUM_PROB, SPEED_INJURY_NONE, SPEED_INJURY_FULCRUM, finalSpeed ); + } else { + probInjury = assignProb ( INJURY_FULCRUM_PROB, 96, SPEED_INJURY_FULCRUM, SPEED_FATALITY_ALL, finalSpeed ); + } + fatalities = 0; + state->injured = 0; + for ( i = 0; i < CREWS_COMPLIMENT; i++ ) { + myRand = gen_random_int ( 1, 100 ); + if ( myRand <= probFatal ) fatalities++; + else if ( myRand <= probInjury ) state->injured++; + } + state->uninjured = CREWS_COMPLIMENT - fatalities - state->injured; + } + // 4) Special conditions: hero + if (!allDone) { + if ( (shipsHealth>=8) && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) { + state->hero_counter++; + if ( state->hero_counter==1 ) sprintf ( buf, "HErO " ); + else if ( state->hero_counter == LEVEL_ACE ) sprintf ( buf, " ACE " ); + else if ( state->hero_counter == LEVEL_STARBUCK ) sprintf ( buf, "STrbUC" ); + else if ( state->hero_counter>99 ) sprintf ( buf, "HEr%3d", state->hero_counter ); + else sprintf ( buf, "HErO%2d", state->hero_counter ); // Typical case + allDone = true; + // Two rule sets for finding Earth. Alternate between easy and hard. + int8_t my_odds, temp; + if ( state->legend_counter %2 == 0 ) my_odds = (int8_t) state->hero_counter - LEVEL_STARBUCK; // Easy + else { + temp = ( state->hero_counter - LEVEL_STARBUCK ) + HARD_EARTH_INCREMENTS - 1; + my_odds = temp / HARD_EARTH_INCREMENTS; + my_odds = min ( my_odds, MAX_HARD_EARTH_CHANCE ); + } + // Display odds in weekday region if positive value + if ( my_odds > 0 ) { + char buff3 [ 5 ]; + sprintf ( buff3, "%2d", my_odds ); + watch_display_text( WATCH_POSITION_TOP_RIGHT, buff3 ); + } else watch_display_text( WATCH_POSITION_TOP_RIGHT, " " ); + if ( my_odds >= gen_random_int ( 1, 200 ) ) { // EARTH!!!! The final objective. + sprintf ( buf, "EArTH " ); // 17% within 8, 50% by 16, 79% by 24, 94% by 32 <- easy mode + state->hero_counter = 0; + state->legend_counter++; + } + // Recalculate difficulty level base on new hero_counter. + state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL; + state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit + // Write to EEPROM + write_to_lander_EEPROM(state); + } + } + // 5) Set fuel conservation indicators as appropriate + if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) watch_set_indicator ( WATCH_INDICATOR_LAP ); + if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GREAT ) ) watch_set_indicator ( WATCH_INDICATOR_24H ); + if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GOOD ) ) watch_set_indicator ( WATCH_INDICATOR_PM ); + // 6) Set coffee maker OK indicator as appropriate + if ( shipsHealth >= 5 || ( shipsHealth >= 0 && ( gen_random_int ( 0, 3 ) != 1 ) ) ){ + watch_set_indicator ( WATCH_INDICATOR_SIGNAL ); + } + // 7) Green light if ship intact + if ( shipsHealth >= 8 && state->led_enabled) { + watch_set_led_green ( ); + state->led_active = true; + } + // 8) Set standard display if not preempted. + if (!allDone) { + if ( ( state->injured > 0 ) || ( state->uninjured == 0 ) ) { + sprintf ( buf, "%d %2d%2d", shipsHealth, state->uninjured, state->injured ); + } + else { + sprintf ( buf, "%d %2d ", shipsHealth, state->uninjured ); + } + } + // Display final status. + watch_display_text(WATCH_POSITION_BOTTOM, buf ); + } // End if tick_counter == 1 + + // Major crash - ship burning with red LED. + if ( state->ships_health < 0 && state->led_enabled) { + if ( ( gen_random_int ( 0, 1 ) != 1 ) && !last_pass ) { // Always off on last pass + // Turn on red LED. + watch_set_led_red ( ); + state->led_active = true; + } else { + watch_set_led_off ( ); + } + } + // Wait long enough, then allow waiting for next game. + if ( last_pass ) { + watch_set_led_off ( ); + // No change to display text, allow new game to start. + state->mode = MODE_WAITING_TO_START; + // Unless it's time for monsters + uint8_t survivors = state->injured + state->uninjured; + if ( ( state->ships_health >= 0 ) && ( survivors > 0 ) && + ( gen_random_int ( -1, 3 ) >= state->ships_health ) ) { + state->mode = MODE_MONSTER; + state->tick_counter = 0; + state->monster_type = gen_random_int ( 0, MONSTER_TYPES - 1 ); + } + } + } // End if MODE_DISPLAY_FINAL_STATUS + else if ( state->mode == MODE_DISPLAY_SKILL_LEVEL ) { + // Display skill level + if ( state->tick_counter == 1 ) { + sprintf ( buf, " %d", state->skill_level ); + watch_display_text ( WATCH_POSITION_TOP_RIGHT, buf ); + sprintf ( buf, " %d ", state->skill_level ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf ); + } + // Wait long enough, then start game. + if ( state->tick_counter >= ( 2.0 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + // Houston, WE ARE LAUNCHING NOW.... + state->mode = MODE_PLAYING; + } + } + else if ( state->mode == MODE_FIND_EARTH_MESSAGE ) { + // Display "Find" then "Earth" + if ( state->tick_counter == 1 ) { + sprintf ( buf, " FInd " ); + watch_display_text ( WATCH_POSITION_TOP_RIGHT, " " ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf ); + } + if ( state->tick_counter == (int) ( 1.5 * LANDER_TICK_FREQUENCY + 1 ) ) { + sprintf ( buf, "EArTH " ); + watch_display_text ( WATCH_POSITION_TOP_RIGHT, " " ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf ); + } + // Wait long enough, then display skill level. + if ( state->tick_counter >= ( 3 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + state->mode = MODE_DISPLAY_SKILL_LEVEL; + } + } + else if ( state->mode == MODE_MONSTER ) { + if ( state->tick_counter == 1 ) watch_display_text ( WATCH_POSITION_BOTTOM, lander_monster_names[state->monster_type] ); + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS + 1 ) { + uint8_t my_rand; + my_rand = gen_random_int ( 0 , MONSTER_ACTIONS - 1 ); + watch_display_text ( WATCH_POSITION_BOTTOM, lander_monster_actions[my_rand] ); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 ) { // Display 1st monster character + sprintf ( buf, "%s", lander_monster_names[state->monster_type] ); + buf [1] = 0; + watch_display_text(WATCH_POSITION_BOTTOM, buf); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 1 ) { // Display current population, close mouth + sprintf ( buf, " c%2d%2d", state->uninjured, state->injured ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf ); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 3 ) watch_display_character ( 'C', 5 ); // Open mouth + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 5 ) { + // Decision to: continue loop, end loop or eat astronaut + uint8_t survivors = state->injured + state->uninjured; + uint8_t myRand = gen_random_int ( 0, 16 ); + if ( survivors == 0 ) state->mode = MODE_WAITING_TO_START; + else if ( myRand <= 1 ) { // Leave loop with survivors + sprintf ( buf, "%d %2d%2d", state->ships_health, state->uninjured, state->injured ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf); + state->mode = MODE_WAITING_TO_START; + } else if ( myRand <= 11 ) state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Do nothing, loop continues + else { // Eat an astronaut - welcome to the space program! + if ( state->injured > 0 && state->uninjured > 0 ) { + if ( gen_random_int ( 0,1 ) == 0 ) state->injured--; + else state->uninjured--; + } + else if ( state->injured > 0 ) state->injured--; + else state->uninjured--; + state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Re-display + } + } + else if ( state->tick_counter >= MONSTER_DISPLAY_TICKS * 4 ) state->mode = MODE_WAITING_TO_START; // Safety + } // End if MODE_MONSTER + break; // End case EVENT_TICK + case EVENT_ALARM_BUTTON_DOWN: + if ( state->mode == MODE_WAITING_TO_START ) { + // That was the go signal - start a new game!! + float numerator, denominator, timeSquared; + int16_t gravity, thrust; + float myTime, distToTop, fuel_mult; + uint8_t skill_level; + int32_t tplTop; // Top lander height for TPL calculations + movement_request_tick_frequency(LANDER_TICK_FREQUENCY); + watch_set_led_off ( ); // Safety + watch_clear_all_indicators ( ); + // Randomize starting parameters + state->height = gen_random_int ( 131, 181 ) * 80; + // Per line below; see Mars Orbiter September 23, 1999 + if ( gen_random_int ( 0, 8 ) == 5 ) state->height = gen_random_int ( 240, 800 ) * 80; + state->speed = gen_random_int ( -120, 35 ); // Positive is up + state->gravity = gen_random_int ( -3, -2 ) * 2; // negative downwards value + skill_level = gen_random_int ( 1, 4 ); // Precursor to fuel allocation + // Theoretical Perfect Landing (TPL) calculations start here. + myTime = (float) state->speed / (float) state->gravity; // How long to reach this speed? Don't care which way sign is. + distToTop = fabs ( 0.5 * state->gravity * myTime * myTime ); + tplTop = (int) ( state->height + distToTop + 0.5 ); // Theoretical highest point based on all of speed, height and gravity. + // Time squared = ( 2 * grav * height ) / ( t*t + g*t ), where t is net acceleration with thrust on. + gravity = abs ( state->gravity ); + thrust = ENGINE_THRUST + state->gravity; + numerator = 2.0 * (float) gravity * (float) tplTop; + denominator = thrust * thrust + thrust * gravity; + timeSquared = numerator / denominator; + state->fuel_tpl = (int) ( sqrt ( timeSquared ) + 0.5 ); // Fuel required for theoretical perfect landing (TPL). + if ( skill_level == 1 ) fuel_mult = 4.0; // TPL + 300% + else if ( skill_level == 2 ) fuel_mult = 2.5; // TPL + 150% + else if ( skill_level == 3 ) fuel_mult = 1.6; // TPL + 60% + else fuel_mult = 1.3; // TPL + 30% + state->fuel_start = state->fuel_tpl * fuel_mult; + state->fuel_remaining = state->fuel_start; + state->skill_level = skill_level; + state->tick_counter = 0; + if ( gen_random_int ( 1, 109 ) != 37 ) { + // Houston, approaching launch.... + state->mode = MODE_DISPLAY_SKILL_LEVEL; + } + else state->mode = MODE_FIND_EARTH_MESSAGE; + } + break; + case EVENT_LIGHT_BUTTON_DOWN: + if ( state->mode == MODE_WAITING_TO_START ) { + // Display difficulty level + watch_display_text(WATCH_POSITION_BOTTOM, lander_difficulty_names [state->difficulty_level]); + } + break; + case EVENT_LIGHT_LONG_PRESS: + if ( state->mode != MODE_WAITING_TO_START ) break; + state->led_enabled = !state->led_enabled; + if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL); + else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + break; + case EVENT_LIGHT_LONG_UP: + if ( ( state->mode == MODE_WAITING_TO_START ) && ( state->legend_counter > 0 ) ) { + if ( state->legend_counter > 9 ) sprintf (buf,"EArt%2d", state->legend_counter ); + else sprintf (buf,"EArth%d", state->legend_counter ); + // Display legend counter + watch_display_text(WATCH_POSITION_BOTTOM, buf); + } + break; + + default: + movement_default_loop_handler(event); + break; + } + if ( !state->led_active ) return true; + else return false; +} + +void lander_face_resign(void *context) { + (void) context; + watch_set_led_off ( ); +} \ No newline at end of file diff --git a/watch-faces/complication/lander_face.h b/watch-faces/complication/lander_face.h new file mode 100644 index 00000000..5e4c344f --- /dev/null +++ b/watch-faces/complication/lander_face.h @@ -0,0 +1,152 @@ +/* + * MIT License + * + * Copyright (c) 2024 Klingon Jane + * + * 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 LANDER_FACE_H_ +#define LANDER_FACE_H_ + +#include "movement.h" + +/* + +My remake of a classic planet landing game. + +Objective: Safely land the Cringeworthy. +Use your limited fuel supply to achieve a soft touch-down. + +End scenarios and ship's health: + +Hero They name this planet after you. +8 Life is very cozy. +7 +6 +5 Life is tolerable, plus some creature comforts +4 +3 Marooned. +2 +1 +0 Ship destroyed. Life is harsh, no shelter. Giant hamsters are cute. ** +Bug As in squished. +Crater They name this crater after you. +Gone As in vapourized. + +Landing display format is: +Ship's health, intact crewmen, injured crewmen. + +Additional data: +Crew's compliment: 13. +Low fuel warning icons: activates when 3 seconds of full thrust remains. +** Yes, hamsters are very cute. However; some eating of astronauts may occur. + +Starting velocity, height and gravity are randomized each scenario. +Fuel levels randomly assigned from 1 to 4 (hardest) to match starting parameters. + +A safe landing is always possible. + +End of game icons: +LAP - Fantastic budgeting of fuel supply ( Required for heroic landing status. ) +24H - Great budgeting of fuel supply +PM - Good budgeting of fuel supply +SIGNAL - The combination coffee and tea maker survived + +Landings get progressively harder with the number of heroic landings made. +Number of heroic landings are remembered. + +Heroic +Landings Status + 0 Normal + 3 Hard ( first difficulty increase ) + 6 Harder ( final difficulty increase ) + 8 Ace + 11 ?????? + +Save yourself. Save the coffee maker. + +END of standard training manual + +*/ + + +/* + +What is really going on here? +The fleet is lost. You are a newbie pilot making a name for yourself. + +Objective: Find Earth. + +After reaching ?????? status, future heroic sorties will have 'some' chance in 200 +of finding Earth. + +Your chances improve by 1 chance in 200 for each subsequent Heroic Landing (HL). + +Completing HL 12 will give you 1 chance in 200, for that landing. +HL 13 will give you 2 chances in 200, for that landing. +HL 14 will give you 3 chances in 200, for that landing. +HL 20 will give you 9 chances in 200, for that landing, and so on. + +At these higher levels, your chances in 200 are displayed in the upper right corner on a heroic landing. + +For wannabe pilots only: The HL counter can be reset by crashing three consecutive +missions without touching the thrust button. ( 6 to reset Earth-found counter ) + +Find Earth. Save Humanity. + +*/ + +typedef struct { + int32_t height; + int16_t speed; // Positive is up + uint16_t tick_counter; // For minimum delays + uint16_t fuel_start; + uint16_t fuel_remaining; + uint16_t fuel_tpl; // Fuel required for theoretical perfect landing + uint16_t fuel_score; // 100 is perfect; higher is less perfect + int8_t gravity; // negative downwards value + bool led_enabled; // Can the led be turned on? + bool led_active; // Did we use it this scenario? + uint8_t mode; // 0 Pre-launch waiting, 1 show level, 2 playing, 3 touchdown blank, 4 final display, 5 monster + uint8_t skill_level; // 1 thru 4. Dictates fuel alloted + int8_t ships_health; // 0 thru 8. -1 = major crash + uint8_t hero_counter; // Total heroic landings ever + uint8_t legend_counter; // Historic events counter ( Earth ) + uint8_t difficulty_level; // Based on hero_counter + uint8_t reset_counter; // Can reset hero_counter by crashing using zero fuel several consecutive scenarios + uint8_t monster_type; // Which monster is hungry? + uint8_t uninjured; // OK survivors + uint8_t injured; // Hurt survivors +} lander_state_t; + +void lander_face_setup(uint8_t watch_face_index, void ** context_ptr); +void lander_face_activate(void *context); +bool lander_face_loop(movement_event_t event, void *context); +void lander_face_resign(void *context); + +#define lander_face ((const watch_face_t){ \ + lander_face_setup, \ + lander_face_activate, \ + lander_face_loop, \ + lander_face_resign, \ + NULL, \ +}) + +#endif // LANDER_FACE_H_ diff --git a/watch-faces/complication/moon_phase_face.c b/watch-faces/complication/moon_phase_face.c index 9e9591d3..c7a550dc 100644 --- a/watch-faces/complication/moon_phase_face.c +++ b/watch-faces/complication/moon_phase_face.c @@ -184,10 +184,19 @@ bool moon_phase_face_loop(movement_event_t event, void *context) { state->offset += 86400; _update(state, state->offset); break; - case EVENT_ALARM_LONG_PRESS: - state->offset = 0; + case EVENT_ALARM_LONG_PRESS: + state->offset = 0; _update(state, state->offset); - break; + break; + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_LIGHT_BUTTON_UP: + state->offset -= 86400; + _update(state, state->offset); + break; + case EVENT_LIGHT_LONG_PRESS: + movement_illuminate_led(); + break; case EVENT_TIMEOUT: // QUESTION: Should timeout reset offset to 0? break; diff --git a/watch-faces/complication/moon_phase_face.h b/watch-faces/complication/moon_phase_face.h index b0209829..2a32ed6b 100644 --- a/watch-faces/complication/moon_phase_face.h +++ b/watch-faces/complication/moon_phase_face.h @@ -47,6 +47,9 @@ * each button press, and both the text and the graphical representation will * display the moon phase for that day. Try pressing the Alarm button 27 times * now, just to visualize what the moon will look like over the next month. + * Pressing the Light button will move back in time. + * + * Holding the Light button will illuminate the display. */ #include "movement.h" diff --git a/watch-faces/complication/pulsometer_face.c b/watch-faces/complication/pulsometer_face.c new file mode 100644 index 00000000..dcef39c9 --- /dev/null +++ b/watch-faces/complication/pulsometer_face.c @@ -0,0 +1,216 @@ +/* SPDX-License-Identifier: MIT */ + +/* + * MIT License + * + * Copyright © 2021-2022 Joey Castillo + * Copyright © 2023 Jeremy O'Brien + * Copyright © 2024 Matheus Afonso Martins Moreira (https://www.matheusmoreira.com/) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include + +#include "pulsometer_face.h" +#include "watch.h" +#include "watch_common_display.h" + +#ifndef PULSOMETER_FACE_CALIBRATION_DEFAULT +#define PULSOMETER_FACE_CALIBRATION_DEFAULT (30) +#endif + +#ifndef PULSOMETER_FACE_CALIBRATION_INCREMENT +#define PULSOMETER_FACE_CALIBRATION_INCREMENT (10) +#endif + +// tick frequency will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.) +#ifndef PULSOMETER_FACE_FREQUENCY_FACTOR +#define PULSOMETER_FACE_FREQUENCY_FACTOR (4ul) +#endif + +#define PULSOMETER_FACE_FREQUENCY (1 << PULSOMETER_FACE_FREQUENCY_FACTOR) + +typedef struct { + bool measuring; + int16_t pulses; + int16_t ticks; + int8_t calibration; +} pulsometer_state_t; + +static inline bool lcd_is_custom(void) { + return watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; +} + +static void pulsometer_display_title(pulsometer_state_t *pulsometer) { + (void) pulsometer; + watch_display_text_with_fallback(WATCH_POSITION_TOP, "PULSE", "PL"); +} + +static void pulsometer_display_calibration(pulsometer_state_t *pulsometer) { + char buf[3]; + if (lcd_is_custom()) { + snprintf(buf, sizeof(buf), "%2hhd", pulsometer->calibration); + watch_display_text(WATCH_POSITION_SECONDS, buf); + } else { + snprintf(buf, sizeof(buf), "%2hhd", pulsometer->calibration); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); + } +} + +static void pulsometer_display_measurement(pulsometer_state_t *pulsometer) { + if (lcd_is_custom()) { + char buf[5]; + int16_t value = pulsometer->pulses; + + if (value < 0) value = 0; + if (value > 9999) value = 9999; + + snprintf(buf, sizeof(buf), "%-4hd", value); + watch_display_text(WATCH_POSITION_BOTTOM, buf); + } else { + char buf[7]; + snprintf(buf, sizeof(buf), "%-6hd", pulsometer->pulses); + watch_display_text(WATCH_POSITION_BOTTOM, buf); + } +} + +static void pulsometer_indicate(pulsometer_state_t *pulsometer) { + if (pulsometer->measuring) { + watch_set_indicator(WATCH_INDICATOR_LAP); + } else { + watch_clear_indicator(WATCH_INDICATOR_LAP); + } +} + +static void pulsometer_start_measurement(pulsometer_state_t *pulsometer) { + pulsometer->measuring = true; + pulsometer->pulses = INT16_MAX; + pulsometer->ticks = 0; + + pulsometer_indicate(pulsometer); + + movement_request_tick_frequency(PULSOMETER_FACE_FREQUENCY); +} + +static void pulsometer_measure(pulsometer_state_t *pulsometer) { + if (!pulsometer->measuring) { return; } + + pulsometer->ticks++; + + float ticks_per_minute = 60 << PULSOMETER_FACE_FREQUENCY_FACTOR; + float pulses_while_button_held = ticks_per_minute / pulsometer->ticks; + float calibrated_pulses = pulses_while_button_held * pulsometer->calibration; + calibrated_pulses += 0.5f; + + pulsometer->pulses = (int16_t) calibrated_pulses; + + pulsometer_display_measurement(pulsometer); +} + +static void pulsometer_stop_measurement(pulsometer_state_t *pulsometer) { + movement_request_tick_frequency(1); + + pulsometer->measuring = false; + + pulsometer_display_measurement(pulsometer); + pulsometer_indicate(pulsometer); +} + +static void pulsometer_cycle_calibration(pulsometer_state_t *pulsometer, int8_t increment) { + if (pulsometer->measuring) { return; } + + if (pulsometer->calibration <= 0) { + pulsometer->calibration = 1; + } + + int8_t last = pulsometer->calibration; + pulsometer->calibration += increment; + + if (pulsometer->calibration > 39) { + pulsometer->calibration = last == 39? 1 : 39; + } + + pulsometer_display_calibration(pulsometer); +} + +void pulsometer_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + + if (*context_ptr == NULL) { + pulsometer_state_t *pulsometer = malloc(sizeof(pulsometer_state_t)); + + pulsometer->calibration = PULSOMETER_FACE_CALIBRATION_DEFAULT; + pulsometer->pulses = 0; + pulsometer->ticks = 0; + + *context_ptr = pulsometer; + } +} + +void pulsometer_face_activate(void *context) { + + pulsometer_state_t *pulsometer = context; + + pulsometer->measuring = false; + + pulsometer_display_title(pulsometer); + pulsometer_display_calibration(pulsometer); + pulsometer_display_measurement(pulsometer); +} + +bool pulsometer_face_loop(movement_event_t event,void *context) { + + pulsometer_state_t *pulsometer = (pulsometer_state_t *) context; + + switch (event.event_type) { + case EVENT_ALARM_BUTTON_DOWN: + pulsometer_start_measurement(pulsometer); + break; + case EVENT_ALARM_BUTTON_UP: + case EVENT_ALARM_LONG_UP: + pulsometer_stop_measurement(pulsometer); + break; + case EVENT_TICK: + pulsometer_measure(pulsometer); + break; + case EVENT_LIGHT_BUTTON_UP: + pulsometer_cycle_calibration(pulsometer, 1); + break; + case EVENT_LIGHT_LONG_UP: + pulsometer_cycle_calibration(pulsometer, PULSOMETER_FACE_CALIBRATION_INCREMENT); + break; + case EVENT_LIGHT_BUTTON_DOWN: + // Inhibit the LED + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +void pulsometer_face_resign(void *context) { + (void) context; +} diff --git a/watch-faces/complication/pulsometer_face.h b/watch-faces/complication/pulsometer_face.h new file mode 100644 index 00000000..7adb9ca8 --- /dev/null +++ b/watch-faces/complication/pulsometer_face.h @@ -0,0 +1,87 @@ +/* SPDX-License-Identifier: MIT */ + +/* + * MIT License + * + * Copyright © 2021-2022 Joey Castillo + * Copyright © 2022 Alexsander Akers + * Copyright © 2023 Alex Utter + * Copyright © 2024 Matheus Afonso Martins Moreira (https://www.matheusmoreira.com/) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * 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 PULSOMETER_FACE_H_ +#define PULSOMETER_FACE_H_ + +/* + * PULSOMETER face + * + * The pulsometer implements a classic mechanical watch complication. + * A mechanical pulsometer involves a chronograph with a scale that + * allows the user to compute the number of heart beats per minute + * in less time. The scale is calibrated, or graduated, for a fixed + * number of heart beats, most often 30. The user starts the chronograph + * and simultaneously begins counting the heart beats. The movement of + * the chronograph's seconds hand over time automatically performs the + * computations required. When the calibrated number of heart beats + * is reached, the chronograph is stopped and the seconds hand shows + * the heart rate. + * + * The Sensor Watch pulsometer improves this design with user calibration: + * it can be graduated to any value between 1 and 39 pulsations per minute. + * The default is still 30, mirroring the classic pulsometer calibration. + * This feature allows the user to reconfigure the pulsometer to count + * many other types of periodic minutely events, making it more versatile. + * For example, it can be set to 5 respirations per minute to turn it into + * an asthmometer, a nearly identical mechanical watch complication + * that doctors might use to quickly measure respiratory rate. + * + * To use the pulsometer, hold the ALARM button and count the pulses. + * When the calibrated number of pulses is reached, release the button. + * The display will show the number of pulses per minute. + * + * In order to measure heart rate, feel for a pulse using the hand with + * the watch while holding the button down with the other. + * The pulse can be easily felt on the carotid artery of the neck. + * + * In order to measure breathing rate, simply hold the ALARM button + * and count the number of breaths. + * + * To calibrate the pulsometer, press LIGHT + * to cycle to the next integer calibration. + * Long press LIGHT to cycle it by 10. + */ + +#include "movement.h" + +void pulsometer_face_setup(uint8_t watch_face_index, void ** context_ptr); +void pulsometer_face_activate(void *context); +bool pulsometer_face_loop(movement_event_t event,void *context); +void pulsometer_face_resign(void *context); + +#define pulsometer_face ((const watch_face_t){ \ + pulsometer_face_setup, \ + pulsometer_face_activate, \ + pulsometer_face_loop, \ + pulsometer_face_resign, \ + NULL, \ +}) + +#endif // PULSOMETER_FACE_H_ diff --git a/legacy/watch_faces/complication/simon_face.c b/watch-faces/complication/simon_face.c similarity index 90% rename from legacy/watch_faces/complication/simon_face.c rename to watch-faces/complication/simon_face.c index 41bc5c3d..642b32a2 100644 --- a/legacy/watch_faces/complication/simon_face.c +++ b/watch-faces/complication/simon_face.c @@ -23,6 +23,7 @@ */ #include "simon_face.h" +#include "delay.h" #include #include #include @@ -47,18 +48,19 @@ static inline uint8_t _simon_get_rand_num(uint8_t num_values) { } 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); + watch_clear_display(); + if (state->playing_state != SIMON_NOT_PLAYING) { + sprintf(_simon_display_buf, "%2d", state->sequence_length); + watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf); } } static void _simon_not_playing_display(simon_state_t *state) { _simon_clear_display(state); - sprintf(_simon_display_buf, "SI %d", state->best_score); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "SIMON", "SI"); + sprintf(_simon_display_buf, "%d", state->best_score); + watch_display_text(WATCH_POSITION_BOTTOM, _simon_display_buf); if (!state->soundOff) watch_set_indicator(WATCH_INDICATOR_BELL); else @@ -67,14 +69,13 @@ static void _simon_not_playing_display(simon_state_t *state) { 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); + watch_display_text(WATCH_POSITION_SECONDS, " E"); break; case SIMON_MODE_HARD: - watch_display_string("H", 9); + watch_display_text(WATCH_POSITION_SECONDS, " H"); break; default: break; @@ -90,24 +91,27 @@ static void _simon_reset(simon_state_t *state) { static void _simon_display_note(SimonNote note, simon_state_t *state) { - char *ndtemplate = NULL; - + watch_clear_display(); + if (note == SIMON_WRONG_NOTE) { + watch_display_text(WATCH_POSITION_TOP_LEFT, "OH"); + watch_display_text(WATCH_POSITION_BOTTOM, "NOOOOO"); + return; + } + sprintf(_simon_display_buf, "%2d", state->sequence_length); + watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf); switch (note) { case SIMON_LED_NOTE: - ndtemplate = "LI%2d "; + watch_display_text(WATCH_POSITION_TOP_LEFT, "LI"); break; case SIMON_ALARM_NOTE: - ndtemplate = " %2d AL"; + watch_display_text(WATCH_POSITION_SECONDS, "AL"); break; case SIMON_MODE_NOTE: - ndtemplate = " %2dDE "; + watch_display_text_with_fallback(WATCH_POSITION_HOURS, "Md", "DE"); + break; + default: 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) { @@ -220,7 +224,6 @@ void simon_face_setup(uint8_t watch_face_index, } void simon_face_activate(void *context) { - (void) settings; (void) context; simon_state_t *state = (simon_state_t *)context; _simon_change_speed(state); diff --git a/legacy/watch_faces/complication/simon_face.h b/watch-faces/complication/simon_face.h similarity index 99% rename from legacy/watch_faces/complication/simon_face.h rename to watch-faces/complication/simon_face.h index 63e83895..a3dd743b 100644 --- a/legacy/watch_faces/complication/simon_face.h +++ b/watch-faces/complication/simon_face.h @@ -95,8 +95,8 @@ void simon_face_activate(void *context); bool simon_face_loop(movement_event_t event, void *context); void simon_face_resign(void *context); -#define simon_face \ - ((const watch_face_t){ \ +#define simon_face \ + ((const watch_face_t){ \ simon_face_setup, \ simon_face_activate, \ simon_face_loop, \ diff --git a/watch-faces/complication/simple_coin_flip_face.c b/watch-faces/complication/simple_coin_flip_face.c new file mode 100644 index 00000000..635c70ff --- /dev/null +++ b/watch-faces/complication/simple_coin_flip_face.c @@ -0,0 +1,180 @@ +/* + * MIT License + * + * Copyright (c) 2023 Wesley Aptekar-Cassels + * Copyright (c) 2025 Vaipex + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include "simple_coin_flip_face.h" +#include "delay.h" + +void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(simple_coin_flip_face_state_t)); + memset(*context_ptr, 0, sizeof(simple_coin_flip_face_state_t)); + } +} + +void simple_coin_flip_face_activate(void *context) { + (void) context; +} + +static uint32_t get_random(uint32_t max) { + #if __EMSCRIPTEN__ + return rand() % max; + #else + return arc4random_uniform(max); + #endif + +} + +static void draw_start_face(void) { + watch_clear_display(); + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { + watch_display_text(WATCH_POSITION_BOTTOM, " Flip"); + } else { + watch_display_text(WATCH_POSITION_BOTTOM, "Flip"); + } +} + +static void set_pixels(int pixels[3][4][2], int j_len) { + for(int loopruns = 0; loopruns<2; loopruns++) { + for(int i = 0; i<3; i++) { + watch_clear_display(); + for(int j = 0; jis_start_face && !state->active){ + if(state->inactivity_ticks >= 15){ + state->is_start_face = false; + state->inactivity_ticks = 0; + draw_start_face(); + }else{ + state->inactivity_ticks++; + } + } else { + state->inactivity_ticks = 0; + } + break; + //execute same action for light and alarm button + case EVENT_LIGHT_BUTTON_UP: + case EVENT_ALARM_BUTTON_UP: + if (!state->active) { + state->active = true; + _blink_face_update_lcd(state); + state->active = false; + state->is_start_face = false; + state->inactivity_ticks = 0; + } + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + case EVENT_LOW_ENERGY_UPDATE: + break; + default: + return movement_default_loop_handler(event); + } + + return true; +} + +void simple_coin_flip_face_resign(void *context) { + (void) context; +} + diff --git a/watch-faces/complication/simple_coin_flip_face.h b/watch-faces/complication/simple_coin_flip_face.h new file mode 100644 index 00000000..f5c95a4b --- /dev/null +++ b/watch-faces/complication/simple_coin_flip_face.h @@ -0,0 +1,60 @@ +/* + * MIT License + * + * Copyright (c) 2023 Wesley Aptekar-Cassels + * Copyright (c) 2025 Vaipex + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "movement.h" + +/* + * A extremely simple coin flip face updated from Wesley Aptekar-Cassels version. + * + * Press ALARM or LIGHT to flip a coin, after a short animation it will display + * "Heads" or "Tails". Press ALARM or LIGHT to flip again. + * + * This is for people who want a simpler UI than probability_face or + * randonaut_face. While those have more features, this one is more immediately + * obvious - useful, for instance, if you are using a coin flip to agree on + * something with someone, and want the operation to be clear to someone who + * has not had anything explained to them. + */ + +typedef struct { + bool active; + bool is_start_face; + uint8_t inactivity_ticks; +} simple_coin_flip_face_state_t; + +void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr); +void simple_coin_flip_face_activate(void *context); +bool simple_coin_flip_face_loop(movement_event_t event, void *context); +void simple_coin_flip_face_resign(void *context); + +#define simple_coin_flip_face ((const watch_face_t){ \ + simple_coin_flip_face_setup, \ + simple_coin_flip_face_activate, \ + simple_coin_flip_face_loop, \ + simple_coin_flip_face_resign, \ + NULL, \ +}) diff --git a/watch-faces/complication/stopwatch_face.c b/watch-faces/complication/stopwatch_face.c index e7e5ebb1..55186517 100644 --- a/watch-faces/complication/stopwatch_face.c +++ b/watch-faces/complication/stopwatch_face.c @@ -114,6 +114,7 @@ bool stopwatch_face_loop(movement_event_t event, void *context) { stopwatch_state->start_time.reg = 0; stopwatch_state->seconds_counted = 0; watch_display_text(WATCH_POSITION_BOTTOM, "000000"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); } break; case EVENT_ALARM_BUTTON_DOWN: diff --git a/watch-faces/complication/tarot_face.c b/watch-faces/complication/tarot_face.c index c10b7710..ba50ed6d 100644 --- a/watch-faces/complication/tarot_face.c +++ b/watch-faces/complication/tarot_face.c @@ -280,6 +280,9 @@ void tarot_face_setup(uint8_t watch_face_index, void ** context_ptr) { if (*context_ptr == NULL) { *context_ptr = malloc(sizeof(tarot_state_t)); memset(*context_ptr, 0, sizeof(tarot_state_t)); + tarot_state_t *state = (tarot_state_t *)*context_ptr; + state->major_arcana_only = true; + state->num_cards_to_draw = 3; } // Emulator only: Seed random number generator #if __EMSCRIPTEN__ @@ -292,8 +295,6 @@ void tarot_face_activate(void *context) { watch_display_text_with_fallback(WATCH_POSITION_TOP, "Tarot", "TA"); init_deck(state); - state->num_cards_to_draw = 3; - state->major_arcana_only = true; } bool tarot_face_loop(movement_event_t event, void *context) { diff --git a/legacy/watch_faces/complication/timer_face.c b/watch-faces/complication/timer_face.c similarity index 91% rename from legacy/watch_faces/complication/timer_face.c rename to watch-faces/complication/timer_face.c index de48e356..aaf824c4 100644 --- a/legacy/watch_faces/complication/timer_face.c +++ b/watch-faces/complication/timer_face.c @@ -62,7 +62,8 @@ static void _start(timer_state_t *state, bool with_beep) { } static void _draw(timer_state_t *state, uint8_t subsecond) { - char buf[14]; + char bottom_time[10]; + char timer_id[3]; uint32_t delta; div_t result; uint8_t h, min, sec; @@ -84,37 +85,41 @@ static void _draw(timer_state_t *state, uint8_t subsecond) { result = div(result.quot, 60); min = result.rem; h = result.quot; - sprintf(buf, " %02u%02u%02u", h, min, sec); + sprintf(bottom_time, "%02u%02u%02u", h, min, sec); break; case setting: if (state->settings_state == 1) { // ask it the current timer shall be erased - sprintf(buf, " CLEAR%c", state->erase_timer_flag ? 'y' : 'n'); + sprintf(bottom_time, "CLEAR%c", state->erase_timer_flag ? 'y' : 'n'); watch_clear_colon(); } else if (state->settings_state == 5) { - sprintf(buf, " LOOP%c", state->timers[state->current_timer].unit.repeat ? 'y' : 'n'); + sprintf(bottom_time, " LOOP%c", state->timers[state->current_timer].unit.repeat ? 'y' : 'n'); watch_clear_colon(); } else { - sprintf(buf, " %02u%02u%02u", state->timers[state->current_timer].unit.hours, + sprintf(bottom_time, "%02u%02u%02u", state->timers[state->current_timer].unit.hours, state->timers[state->current_timer].unit.minutes, state->timers[state->current_timer].unit.seconds); watch_set_colon(); } break; case waiting: - sprintf(buf, " %02u%02u%02u", state->timers[state->current_timer].unit.hours, + sprintf(bottom_time, "%02u%02u%02u", state->timers[state->current_timer].unit.hours, state->timers[state->current_timer].unit.minutes, state->timers[state->current_timer].unit.seconds); + watch_set_colon(); break; } - buf[0] = 49 + state->current_timer; + + sprintf(timer_id, "%2u", state->current_timer + 1); if (state->mode == setting && subsecond % 2) { // blink the current settings value - if (state->settings_state == 0) buf[0] = ' '; - else if (state->settings_state == 1 || state->settings_state == 5) buf[6] = ' '; - else buf[(state->settings_state - 1) * 2 - 1] = buf[(state->settings_state - 1) * 2] = ' '; + if (state->settings_state == 0) timer_id[0] = timer_id[1] = ' '; + else if (state->settings_state == 1 || state->settings_state == 5) bottom_time[5] = ' '; + else bottom_time[(state->settings_state - 1) * 2 - 2] = bottom_time[(state->settings_state - 1) * 2 - 1] = ' '; } - watch_display_string(buf, 3); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, bottom_time, bottom_time); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, timer_id, timer_id); + // set lap indicator when we have a looping timer if (state->timers[state->current_timer].unit.repeat) watch_set_indicator(WATCH_INDICATOR_LAP); else watch_clear_indicator(WATCH_INDICATOR_LAP); @@ -200,7 +205,7 @@ void timer_face_setup(uint8_t watch_face_index, void ** context_ptr) { void timer_face_activate(void *context) { timer_state_t *state = (timer_state_t *)context; - watch_display_string("TR", 0); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "TMR", "TR"); watch_set_colon(); if(state->mode == running) { watch_date_time_t now = watch_rtc_get_date_time(); @@ -265,14 +270,14 @@ bool timer_face_loop(movement_event_t event, void *context) { movement_cancel_background_task(); break; case pausing: - _start(statemovement_get_current_timezone_offset(), false); + _start(state, false); break; case waiting: { uint8_t last_timer = state->current_timer; state->current_timer = (state->current_timer + 1) % TIMER_SLOTS; _set_next_valid_timer(state); // start the time immediately if there is only one valid timer slot - if (last_timer == state->current_timer) _start(statemovement_get_current_timezone_offset(), true); + if (last_timer == state->current_timer) _start(state, true); break; } case setting: @@ -299,7 +304,7 @@ bool timer_face_loop(movement_event_t event, void *context) { _beeps_to_play = 4; watch_buzzer_play_sequence((int8_t *)_sound_seq_beep, _signal_callback); _reset(state); - if (state->timers[state->current_timer].unit.repeat) _start(statemovement_get_current_timezone_offset(), false); + if (state->timers[state->current_timer].unit.repeat) _start(state, false); break; case EVENT_ALARM_LONG_PRESS: switch(state->mode) { @@ -319,7 +324,7 @@ bool timer_face_loop(movement_event_t event, void *context) { } break; case waiting: - _start(statemovement_get_current_timezone_offset(), true); + _start(state, true); break; case pausing: case running: diff --git a/legacy/watch_faces/complication/timer_face.h b/watch-faces/complication/timer_face.h similarity index 100% rename from legacy/watch_faces/complication/timer_face.h rename to watch-faces/complication/timer_face.h diff --git a/legacy/watch_faces/complication/totp_face_lfs.c b/watch-faces/complication/totp_lfs_face.c similarity index 87% rename from legacy/watch_faces/complication/totp_face_lfs.c rename to watch-faces/complication/totp_lfs_face.c index 439aca3f..8be3e28d 100644 --- a/legacy/watch_faces/complication/totp_face_lfs.c +++ b/watch-faces/complication/totp_lfs_face.c @@ -33,7 +33,7 @@ #include "watch_utility.h" #include "filesystem.h" -#include "totp_face_lfs.h" +#include "totp_lfs_face.h" #define MAX_TOTP_RECORDS 30 #define MAX_TOTP_SECRET_SIZE 128 @@ -42,7 +42,7 @@ const char* TOTP_URI_START = "otpauth://totp/"; struct totp_record { - char label[2]; + char label[4]; hmac_alg algorithm; uint8_t period; uint8_t secret_size; @@ -67,19 +67,20 @@ static uint8_t num_totp_records = 0; static void init_totp_record(struct totp_record *totp_record) { totp_record->label[0] = 'A'; totp_record->label[1] = 'A'; + totp_record->label[2] = 'A'; + totp_record->label[3] = 0; totp_record->algorithm = SHA1; totp_record->period = 30; totp_record->secret_size = 0; } -static bool totp_face_lfs_read_param(struct totp_record *totp_record, char *param, char *value) { +static bool totp_lfs_face_read_param(struct totp_record *totp_record, char *param, char *value) { if (!strcmp(param, "issuer")) { - if (value[0] == '\0' || value[1] == '\0') { - printf("TOTP issuer must be >= 2 chars, got '%s'\n", value); + if (value[0] == '\0') { + printf("TOTP issuer must be a non-empty string\n"); return false; } - totp_record->label[0] = value[0]; - totp_record->label[1] = value[1]; + snprintf(totp_record->label, sizeof(totp_record->label), "%-3s", value); } else if (!strcmp(param, "secret")) { totp_record->file_secret_length = strlen(value); if (UNBASE32_LEN(totp_record->file_secret_length) > MAX_TOTP_SECRET_SIZE) { @@ -127,7 +128,7 @@ static bool totp_face_lfs_read_param(struct totp_record *totp_record, char *para return true; } -static void totp_face_lfs_read_file(char *filename) { +static void totp_lfs_face_read_file(char *filename) { // For 'format' of file, see comment at top. const size_t uri_start_len = strlen(TOTP_URI_START); @@ -166,7 +167,7 @@ static void totp_face_lfs_read_file(char *filename) { do { char *param_middle = strchr(param, '='); *param_middle = '\0'; - if (totp_face_lfs_read_param(&totp_records[num_totp_records], param, param_middle + 1)) { + if (totp_lfs_face_read_param(&totp_records[num_totp_records], param, param_middle + 1)) { if (!strcmp(param, "secret")) { totp_records[num_totp_records].file_secret_offset = old_offset + (param_middle + 1 - line); } @@ -189,7 +190,7 @@ static void totp_face_lfs_read_file(char *filename) { } } -void totp_face_lfs_setup(uint8_t watch_face_index, void ** context_ptr) { +void totp_lfs_face_setup(uint8_t watch_face_index, void ** context_ptr) { (void) watch_face_index; if (*context_ptr == NULL) { *context_ptr = malloc(sizeof(totp_lfs_state_t)); @@ -197,12 +198,12 @@ void totp_face_lfs_setup(uint8_t watch_face_index, void ** context_ptr) { #if !(__EMSCRIPTEN__) if (num_totp_records == 0) { - totp_face_lfs_read_file(TOTP_FILE); + totp_lfs_face_read_file(TOTP_FILE); } #endif } -static uint8_t *totp_face_lfs_get_file_secret(struct totp_record *record) { +static uint8_t *totp_lfs_face_get_file_secret(struct totp_record *record) { char buffer[BASE32_LEN(MAX_TOTP_SECRET_SIZE) + 1]; int32_t file_secret_offset = record->file_secret_offset; @@ -231,7 +232,7 @@ static void totp_face_set_record(totp_lfs_state_t *totp_state, int i) { record = &totp_records[i]; TOTP( - totp_face_lfs_get_file_secret(record), + totp_lfs_face_get_file_secret(record), record->secret_size, record->period, record->algorithm @@ -240,7 +241,7 @@ static void totp_face_set_record(totp_lfs_state_t *totp_state, int i) { totp_state->steps = totp_state->timestamp / record->period; } -void totp_face_lfs_activate(void *context) { +void totp_lfs_face_activate(void *context) { memset(context, 0, sizeof(totp_lfs_state_t)); totp_lfs_state_t *totp_state = (totp_lfs_state_t *)context; @@ -248,20 +249,20 @@ void totp_face_lfs_activate(void *context) { if (num_totp_records == 0) { // Doing this here rather than in setup makes things a bit more pleasant in the simulator, since there's no easy way to trigger // setup again after uploading the data. - totp_face_lfs_read_file(TOTP_FILE); + totp_lfs_face_read_file(TOTP_FILE); } #endif - totp_state->timestamp = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), movement_get_current_timezone_offset()); + totp_state->timestamp = watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0); totp_face_set_record(totp_state, 0); } static void totp_face_display(totp_lfs_state_t *totp_state) { uint8_t index = totp_state->current_index; - char buf[14]; + char buf[7]; if (num_totp_records == 0) { - watch_display_string("No2F Codes", 0); + watch_display_text(WATCH_POSITION_FULL, "No2F Codes"); return; } @@ -272,12 +273,14 @@ static void totp_face_display(totp_lfs_state_t *totp_state) { } uint8_t valid_for = totp_records[index].period - result.rem; - sprintf(buf, "%c%c%2d%06lu", totp_records[index].label[0], totp_records[index].label[1], valid_for, totp_state->current_code); - - watch_display_string(buf, 0); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, totp_records[index].label, totp_records[index].label); + sprintf(buf, "%2d", valid_for); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + sprintf(buf, "%06lu", totp_state->current_code); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); } -bool totp_face_lfs_loop(movement_event_t event, void *context) { +bool totp_lfs_face_loop(movement_event_t event, void *context) { totp_lfs_state_t *totp_state = (totp_lfs_state_t *)context; @@ -315,6 +318,6 @@ bool totp_face_lfs_loop(movement_event_t event, void *context) { return true; } -void totp_face_lfs_resign(void *context) { +void totp_lfs_face_resign(void *context) { (void) context; } diff --git a/legacy/watch_faces/complication/totp_face_lfs.h b/watch-faces/complication/totp_lfs_face.h similarity index 88% rename from legacy/watch_faces/complication/totp_face_lfs.h rename to watch-faces/complication/totp_lfs_face.h index 16276a2e..3e18a6a8 100644 --- a/legacy/watch_faces/complication/totp_face_lfs.h +++ b/watch-faces/complication/totp_lfs_face.h @@ -61,16 +61,16 @@ typedef struct { uint8_t current_index; } totp_lfs_state_t; -void totp_face_lfs_setup(uint8_t watch_face_index, void ** context_ptr); -void totp_face_lfs_activate(void *context); -bool totp_face_lfs_loop(movement_event_t event, void *context); -void totp_face_lfs_resign(void *context); +void totp_lfs_face_setup(uint8_t watch_face_index, void ** context_ptr); +void totp_lfs_face_activate(void *context); +bool totp_lfs_face_loop(movement_event_t event, void *context); +void totp_lfs_face_resign(void *context); -#define totp_face_lfs ((const watch_face_t){ \ - totp_face_lfs_setup, \ - totp_face_lfs_activate, \ - totp_face_lfs_loop, \ - totp_face_lfs_resign, \ +#define totp_lfs_face ((const watch_face_t){ \ + totp_lfs_face_setup, \ + totp_lfs_face_activate, \ + totp_lfs_face_loop, \ + totp_lfs_face_resign, \ NULL, \ }) diff --git a/legacy/watch_faces/complication/wareki_face.c b/watch-faces/complication/wareki_face.c similarity index 58% rename from legacy/watch_faces/complication/wareki_face.c rename to watch-faces/complication/wareki_face.c index ec87659c..103a503d 100644 --- a/legacy/watch_faces/complication/wareki_face.c +++ b/watch-faces/complication/wareki_face.c @@ -1,9 +1,24 @@ +/* + +The displayed Japanese Era can be changed by the buttons on the watch, making it also usable as a converter between the Gregorian calendar and the Japanese Era. + +Light button: Subtract one year from the Japanese Era. +Start/Stop button: Add one year to the Japanese Era. +Button operations support long-press functionality. + +Japanese Era Notations: + +r : REIWA (令和) +h : HEISEI (平成) +s : SHOWA(昭和) +*/ + #include #include #include "wareki_face.h" #include "filesystem.h" #include "watch_utility.h" - +#include "watch.h" //Long press status flag static bool _alarm_button_press; @@ -14,41 +29,102 @@ void wareki_setup(uint8_t watch_face_index, void ** context_ptr) { (void) watch_face_index; //printf("wareki_setup() \n"); + if (*context_ptr == NULL) { *context_ptr = malloc(sizeof(wareki_state_t)); memset(*context_ptr, 0, sizeof(wareki_state_t)); - // Do any one-time tasks in here; the inside of this conditional happens only at boot. - } + //debug code + // watch_date_time datetime = watch_rtc_get_date_time(); + // datetime.unit.year = 2022 - WATCH_RTC_REFERENCE_YEAR; + // datetime.unit.month = 12; + // datetime.unit.day = 31; + // datetime.unit.hour = 23; + // datetime.unit.minute= 59; + // datetime.unit.second= 30; + // watch_rtc_set_date_time(datetime); + // settings->bit.clock_mode_24h = true; //24時間表記 + // settings->bit.to_interval = 1;//0=60sec 1=2m 2=5m 3=30m + // watch_store_backup_data(settings->reg, 0); + } } // splash view static void draw_wareki_splash(wareki_state_t *state) { (void) state; - char buf[11]; watch_clear_colon(); - sprintf(buf, "%s","wa ------"); - - watch_display_string(buf, 0); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WA ", "wa"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, " "); } //draw year and Japanese wareki static void draw_year_and_wareki(wareki_state_t *state) { - char buf[27]; + char buf[16]; - if(state->disp_year < REIWA_GANNEN){ + bool is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; + + if(state->disp_year == REIWA_GANNEN){ + //The first year of Reiwa (2019) began on May 1. The period before May 1 is Heisei 31. + //In other words, 2019 is Heisei Year 31 and it is possible that it is Reiwa Year 1. + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "H31", " r"); + + if (is_custom_lcd) + { + //For custom LCDs, display both Heisei and Reiwa. + watch_display_text(WATCH_POSITION_TOP_RIGHT, "r1"); + } + else{ + watch_display_text(WATCH_POSITION_TOP_RIGHT, " 1"); + } + } + else if(state->disp_year == HEISEI_GANNEN){ + //The year 1989 could be Showa 64 or it could be Heisei 1. + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "S64", " h"); + + if (is_custom_lcd) + { + //For custom LCDs, display both Showa and Heisei. + watch_display_text(WATCH_POSITION_TOP_RIGHT, "h1"); + } + else{ + watch_display_text(WATCH_POSITION_TOP_RIGHT, " 1"); + } + } + else if(state->disp_year < HEISEI_GANNEN){ + //Showa + //sprintf(buf, " h%2d%4d ", (int)state->disp_year - HEISEI_GANNEN + 1, (int)state->disp_year); + + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Sho", "s "); + + sprintf(buf, "%2d", (int)state->disp_year - SHOWA_GANNEN + 1); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); + } + else if(state->disp_year < REIWA_GANNEN){ //Heisei - sprintf(buf, " h%2d%4d ", (int)state->disp_year - HEISEI_GANNEN + 1, (int)state->disp_year); + //sprintf(buf, " h%2d%4d ", (int)state->disp_year - HEISEI_GANNEN + 1, (int)state->disp_year); + + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "HEI", "h "); + + sprintf(buf, "%2d", (int)state->disp_year - HEISEI_GANNEN + 1); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } else{ //Reiwa - sprintf(buf, " r%2d%4d ", (int)state->disp_year - REIWA_GANNEN + 1 , (int)state->disp_year); + //sprintf(buf, " r%2d%4d ", (int)state->disp_year - REIWA_GANNEN + 1 , (int)state->disp_year); + + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "REI", "r "); + + sprintf(buf, "%2d", (int)state->disp_year - REIWA_GANNEN + 1); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } - watch_display_string(buf, 0); + + sprintf(buf, "%4d ",(int)state->disp_year); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } @@ -81,7 +157,6 @@ void addYear(wareki_state_t* state,int count){ state->disp_year = REIWA_LIMIT; } else{ - //watch_buzzer_play_note(BUZZER_NOTE_C8, 30); } } @@ -90,11 +165,10 @@ void subYear(wareki_state_t* state,int count){ state->disp_year = state->disp_year - count; - if(state->disp_year < 1989 ){ - state->disp_year = 1989; + if(state->disp_year < SHOWA_GANNEN ){ + state->disp_year = SHOWA_GANNEN; } else{ - //watch_buzzer_play_note(BUZZER_NOTE_C7, 30); } } @@ -123,7 +197,7 @@ bool wareki_loop(movement_event_t event, void *context) { //printf("tick %d\n",state->disp_year ); - if (_alarm_button_press && HAL_GPIO_BTN_ALARM_read()){ + if (_alarm_button_press && HAL_GPIO_BTN_ALARM_pin() ){ //printf("ALARM ON\n"); } else{ @@ -131,7 +205,8 @@ bool wareki_loop(movement_event_t event, void *context) { _alarm_button_press = false; } - if (_light_button_press && HAL_GPIO_BTN_LIGHT_read()){ + + if (_light_button_press && HAL_GPIO_BTN_LIGHT_pin()){ //printf("LIGHT ON\n"); } else{ @@ -192,13 +267,12 @@ bool wareki_loop(movement_event_t event, void *context) { //printf("time out ! \n"); movement_move_to_face(0); - break; //case EVENT_LOW_ENERGY_UPDATE: // If you did not resign in EVENT_TIMEOUT, you can use this event to update the display once a minute. // Avoid displaying fast-updating values like seconds, since the display won't update again for 60 seconds. // You should also consider starting the tick animation, to show the wearer that this is sleep mode: - // watch_start_sleep_animation(500); + // watch_start_tick_animation(500); //break; default: // Movement's default loop handler will step in for any cases you don't handle above: @@ -206,6 +280,7 @@ bool wareki_loop(movement_event_t event, void *context) { // * 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 movement_default_loop_handler(event); } diff --git a/legacy/watch_faces/complication/wareki_face.h b/watch-faces/complication/wareki_face.h similarity index 93% rename from legacy/watch_faces/complication/wareki_face.h rename to watch-faces/complication/wareki_face.h index 4dffc46d..070642e8 100644 --- a/legacy/watch_faces/complication/wareki_face.h +++ b/watch-faces/complication/wareki_face.h @@ -3,9 +3,10 @@ #include "movement.h" -#define REIWA_LIMIT 2018 + 31 +#define REIWA_LIMIT 2018 + 99 #define REIWA_GANNEN 2019 #define HEISEI_GANNEN 1989 +#define SHOWA_GANNEN 1926 typedef struct { bool active; @@ -14,11 +15,11 @@ typedef struct { uint32_t real_year; //The actual current year } wareki_state_t; - void wareki_setup(uint8_t watch_face_index, void ** context_ptr); void wareki_activate(void *context); bool wareki_loop(movement_event_t event, void *context); void wareki_resign(void *context); + void addYear(wareki_state_t* state,int count); void subYear(wareki_state_t* state,int count); diff --git a/legacy/watch_faces/complication/wordle_face.c b/watch-faces/complication/wordle_face.c similarity index 75% rename from legacy/watch_faces/complication/wordle_face.c rename to watch-faces/complication/wordle_face.c index a93a0e0e..d63ef467 100644 --- a/legacy/watch_faces/complication/wordle_face.c +++ b/watch-faces/complication/wordle_face.c @@ -26,6 +26,7 @@ #include #include "wordle_face.h" #include "watch_utility.h" +#include "watch_common_display.h" static uint32_t get_random(uint32_t max) { #if __EMSCRIPTEN__ @@ -35,7 +36,7 @@ static uint32_t get_random(uint32_t max) { #endif } -static uint8_t get_first_pos(WordleLetterResult *word_elements_result) { +static uint8_t get_first_pos(wordle_letter_result *word_elements_result) { for (size_t i = 0; i < WORDLE_LENGTH; i++) { if (word_elements_result[i] != WORDLE_LETTER_CORRECT) return i; @@ -43,7 +44,7 @@ static uint8_t get_first_pos(WordleLetterResult *word_elements_result) { return 0; } -static uint8_t get_next_pos(uint8_t curr_pos, WordleLetterResult *word_elements_result) { +static uint8_t get_next_pos(uint8_t curr_pos, wordle_letter_result *word_elements_result) { for (size_t pos = curr_pos; pos < WORDLE_LENGTH;) { if (word_elements_result[++pos] != WORDLE_LETTER_CORRECT) return pos; @@ -51,7 +52,7 @@ static uint8_t get_next_pos(uint8_t curr_pos, WordleLetterResult *word_elements_ return WORDLE_LENGTH; } -static uint8_t get_prev_pos(uint8_t curr_pos, WordleLetterResult *word_elements_result) { +static uint8_t get_prev_pos(uint8_t curr_pos, wordle_letter_result *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) @@ -75,21 +76,21 @@ static void get_prev_letter(const uint8_t curr_pos, uint8_t *word_elements, cons } static void display_letter(wordle_state_t *state, bool display_dash) { - char buf[1 + 1]; + char buf[3]; if (state->word_elements[state->position] >= WORDLE_NUM_VALID_LETTERS) { if (display_dash) - watch_display_string("-", state->position + 5); + watch_display_character('-', state->position + 5); else - watch_display_string(" ", state->position + 5); + watch_display_character(' ', state->position + 5); return; } sprintf(buf, "%c", _valid_letters[state->word_elements[state->position]]); - watch_display_string(buf, state->position + 5); + watch_display_character(buf[0], state->position + 5); } static void display_all_letters(wordle_state_t *state) { uint8_t prev_pos = state->position; - watch_display_string(" ", 4); + watch_display_character(' ', 4); for (size_t i = 0; i < WORDLE_LENGTH; i++) { state->position = i; display_letter(state, false); @@ -99,13 +100,15 @@ static void display_all_letters(wordle_state_t *state) { #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); + state->curr_screen = WORDLE_SCREEN_NO_DICT; + watch_display_text(WATCH_POSITION_BOTTOM, "nodict"); + state->ignore_btn_ticks = WORDLE_TICK_BAD_GUESS; } static void display_already_guessed(wordle_state_t *state) { - state->curr_screen = SCREEN_ALREADY_GUESSED; - watch_display_string("GUESSD", 4); + state->curr_screen = WORDLE_SCREEN_ALREADY_GUESSED; + watch_display_text(WATCH_POSITION_BOTTOM, "GUESSD"); + state->ignore_btn_ticks = WORDLE_TICK_BAD_GUESS; } static uint32_t check_word_in_dict(uint8_t *word_elements) { @@ -164,12 +167,12 @@ static bool check_word(wordle_state_t *state) { return false; } -static void show_skip_wrong_letter_indicator(bool skipping, WordleScreen curr_screen) { - if (curr_screen >= SCREEN_PLAYING) return; +static void show_skip_wrong_letter_indicator(bool skipping, wordle_screen curr_screen) { + if (curr_screen >= WORDLE_SCREEN_PLAYING) return; if (skipping) - watch_display_string("H", 3); + watch_display_character('H', 3); else - watch_display_string(" ", 3); + watch_display_character(' ', 3); } static void update_known_wrong_letters(wordle_state_t *state) { @@ -197,11 +200,11 @@ static void update_known_wrong_letters(wordle_state_t *state) { static void display_attempt(uint8_t attempt) { char buf[3]; sprintf(buf, "%d", attempt+1); - watch_display_string(buf, 3); + watch_display_character(buf[0], 3); } static void display_playing(wordle_state_t *state) { - state->curr_screen = SCREEN_PLAYING; + state->curr_screen = WORDLE_SCREEN_PLAYING; display_attempt(state->attempt); display_all_letters(state); } @@ -230,64 +233,79 @@ static void reset_incorrect_elements(wordle_state_t *state) { } } +static bool is_in_do_not_use_list(uint16_t guess, const uint16_t *not_to_use, uint8_t max_repeats) { + for (size_t i = 0; i < max_repeats; i++) { + if (guess == not_to_use[i]) return true; + } + return false; +} + static void reset_board(wordle_state_t *state) { reset_all_elements(state); - state->curr_answer = get_random(WORDLE_NUM_WORDS); + do { + state->curr_answer = get_random(WORDLE_NUM_WORDS); + } while (is_in_do_not_use_list(state->curr_answer, state->not_to_use, WORDLE_MAX_BETWEEN_REPEATS)); watch_clear_colon(); state->position = get_first_pos(state->word_elements_result); display_playing(state); - watch_display_string(" -", 4); + watch_display_character('-', 5); #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); + state->curr_screen = WORDLE_SCREEN_TITLE; + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, "WordLE"); 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); + watch_display_character(continuing ? 'y' : 'n', 9); } static void display_continue(wordle_state_t *state) { - state->curr_screen = SCREEN_CONTINUE; - watch_display_string("Cont ", 4); + state->curr_screen = WORDLE_SCREEN_CONTINUE; + watch_display_text(WATCH_POSITION_BOTTOM, "Cont "); 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; + char buf[10]; + state->curr_screen = WORDLE_SCREEN_STREAK; #if WORDLE_USE_DAILY_STREAK == 2 if (state->streak > 99) - sprintf(buf, "WO St--dy"); + sprintf(buf, "St--dy"); else - sprintf(buf, "WO St%2ddy", state->streak); + sprintf(buf, "St%2ddy", state->streak); #else - sprintf(buf, "WO St%4d", state->streak); + sprintf(buf, "St%4d", state->streak); #endif - watch_display_string(buf, 0); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, buf); 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; + state->curr_screen = WORDLE_SCREEN_WAIT; if (state->streak < 40) { - char buf[13]; - sprintf(buf,"WO%2d WaIt ", state->streak); - watch_display_string(buf, 0); + char buf[5]; + sprintf(buf,"%2d", state->streak); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } else { // Streak too long to display in top-right - watch_display_string("WO WaIt ", 0); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); } + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO"); + watch_display_text(WATCH_POSITION_BOTTOM, " WaIt "); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); } #endif @@ -301,16 +319,20 @@ static uint32_t get_day_unix_time(void) { } 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); + char buf[10]; + sprintf(buf," %s", subsecond % 2 ? _valid_words[state->curr_answer] : " "); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "LOSE", "L "); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } 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); + char buf[10]; + sprintf(buf," %s ", subsecond % 2 ? "NICE" : "JOb "); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "W "); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } static bool is_playing(const wordle_state_t *state) { @@ -323,38 +345,40 @@ static bool is_playing(const wordle_state_t *state) { } static void display_result(wordle_state_t *state, uint8_t subsecond) { - char buf[WORDLE_LENGTH + 1]; + char buf[10]; + buf[0] = ' '; for (size_t i = 0; i < WORDLE_LENGTH; i++) { switch (state->word_elements_result[i]) { case WORDLE_LETTER_WRONG: - buf[i] = '-'; + buf[i+1] = '-'; break; case WORDLE_LETTER_CORRECT: - buf[i] = _valid_letters[state->word_elements[i]]; + buf[i+1] = _valid_letters[state->word_elements[i]]; break; case WORDLE_LETTER_WRONG_LOC: if (subsecond % 2) - buf[i] = ' '; + buf[i+1] = ' '; else - buf[i] = _valid_letters[state->word_elements[i]]; + buf[i+1] = _valid_letters[state->word_elements[i]]; default: break; } } - watch_display_string(buf, 5); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } -static bool act_on_btn(wordle_state_t *state, const uint8_t pin) { +static bool act_on_btn(wordle_state_t *state, const wordle_pin_enum pin) { + if (state->ignore_btn_ticks > 0) return true; switch (state->curr_screen) { - case SCREEN_RESULT: + case WORDLE_SCREEN_RESULT: reset_incorrect_elements(state); state->position = get_first_pos(state->word_elements_result); display_playing(state); return true; - case SCREEN_TITLE: + case WORDLE_SCREEN_TITLE: #if WORDLE_USE_DAILY_STREAK == 2 if (state->day_last_game_started == get_day_unix_time()) { display_wait(state); @@ -372,26 +396,26 @@ static bool act_on_btn(wordle_state_t *state, const uint8_t pin) { display_streak(state); #endif return true; - case SCREEN_STREAK: + case WORDLE_SCREEN_STREAK: state->day_last_game_started = get_day_unix_time(); reset_board(state); return true; - case SCREEN_WIN: - case SCREEN_LOSE: + case WORDLE_SCREEN_WIN: + case WORDLE_SCREEN_LOSE: display_title(state); return true; - case SCREEN_NO_DICT: - case SCREEN_ALREADY_GUESSED: + case WORDLE_SCREEN_NO_DICT: + case WORDLE_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: + case WORDLE_SCREEN_WAIT: (void) pin; display_title(state); return true; #else - case SCREEN_CONTINUE: + case WORDLE_SCREEN_CONTINUE: switch (pin) { case BTN_ALARM: @@ -407,6 +431,8 @@ static bool act_on_btn(wordle_state_t *state, const uint8_t pin) { state->continuing = !state->continuing; display_continue_result(state->continuing); break; + default: + break; } return true; #endif @@ -416,6 +442,13 @@ static bool act_on_btn(wordle_state_t *state, const uint8_t pin) { return false; } +static void win_lose_shared(wordle_state_t *state) { + reset_all_elements(state); + state->ignore_btn_ticks = WORDLE_TICK_WIN_LOSE; + state->not_to_use[state->not_to_use_position] = state->curr_answer; + state->not_to_use_position = (state->not_to_use_position + 1) % WORDLE_MAX_BETWEEN_REPEATS; +} + static void get_result(wordle_state_t *state) { #if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES // Check if it's in the dict @@ -437,8 +470,8 @@ static void get_result(wordle_state_t *state) { #endif bool exact_match = check_word(state); if (exact_match) { - reset_all_elements(state); - state->curr_screen = SCREEN_WIN; + state->curr_screen = WORDLE_SCREEN_WIN; + win_lose_shared(state); if (state->streak < 0x7F) state->streak++; #if WORDLE_USE_DAILY_STREAK == 2 @@ -447,13 +480,14 @@ static void get_result(wordle_state_t *state) { return; } if (++state->attempt >= WORDLE_MAX_ATTEMPTS) { - reset_all_elements(state); - state->curr_screen = SCREEN_LOSE; + state->curr_screen = WORDLE_SCREEN_LOSE; + win_lose_shared(state); state->streak = 0; return; } update_known_wrong_letters(state); - state->curr_screen = SCREEN_RESULT; + state->curr_screen = WORDLE_SCREEN_RESULT; + state->ignore_btn_ticks = WORDLE_TICKS_RESULT; return; } @@ -476,21 +510,7 @@ static void insert_random_guess(wordle_state_t *state) { } #endif -void wordle_face_setup(uint8_t watch_face_index, void ** context_ptr) { - (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(void *context) { - wordle_state_t *state = (wordle_state_t *)context; +static void _activate(wordle_state_t *state) { #if WORDLE_USE_DAILY_STREAK != 0 uint32_t now = get_day_unix_time(); uint32_t one_day = 60 *60 * 24; @@ -501,35 +521,58 @@ void wordle_face_activate(void *context) { } #endif state->using_random_guess = false; - if (is_playing(state) && state->curr_screen >= SCREEN_RESULT) { + if (is_playing(state) && state->curr_screen >= WORDLE_SCREEN_RESULT) { reset_incorrect_elements(state); state->position = get_first_pos(state->word_elements_result); } - movement_request_tick_frequency(2); + movement_request_tick_frequency(WORDLE_FREQ); + watch_clear_all_indicators(); + watch_clear_colon(); display_title(state); } +void wordle_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (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 = WORDLE_SCREEN_TITLE; + state->skip_wrong_letter = true; + reset_all_elements(state); + memset(state->not_to_use, 0xff, sizeof(state->not_to_use)); + } + // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. +} + +void wordle_face_activate(void *context) { + wordle_state_t *state = (wordle_state_t *)context; + _activate(state); + +} + bool wordle_face_loop(movement_event_t event, void *context) { wordle_state_t *state = (wordle_state_t *)context; switch (event.event_type) { case EVENT_TICK: + if (state->ignore_btn_ticks > 0) state->ignore_btn_ticks--; switch (state->curr_screen) { - case SCREEN_PLAYING: + case WORDLE_SCREEN_PLAYING: if (event.subsecond % 2) { display_letter(state, true); } else { - watch_display_string(" ", state->position + 5); + watch_display_character(' ', state->position + 5); } break; - case SCREEN_RESULT: + case WORDLE_SCREEN_RESULT: display_result(state, event.subsecond); break; - case SCREEN_LOSE: + case WORDLE_SCREEN_LOSE: display_lose(state, event.subsecond); break; - case SCREEN_WIN: + case WORDLE_SCREEN_WIN: display_win(state, event.subsecond); break; default: @@ -542,12 +585,12 @@ bool wordle_face_loop(movement_event_t event, void *context) { display_letter(state, true); break; case EVENT_LIGHT_LONG_PRESS: - if (state->curr_screen < SCREEN_PLAYING) { + if (state->curr_screen < WORDLE_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; + if (state->curr_screen != WORDLE_SCREEN_PLAYING) break; get_prev_letter(state->position, state->word_elements, state->known_wrong_letters, state->skip_wrong_letter); display_letter(state, true); break; @@ -569,7 +612,7 @@ bool wordle_face_loop(movement_event_t event, void *context) { } break; case EVENT_ALARM_LONG_PRESS: - if (state->curr_screen != SCREEN_PLAYING) break; + if (state->curr_screen != WORDLE_SCREEN_PLAYING) break; display_letter(state, true); state->position = get_prev_pos(state->position, state->word_elements_result); break; @@ -577,17 +620,24 @@ bool wordle_face_loop(movement_event_t event, void *context) { case EVENT_ACTIVATE: break; case EVENT_TIMEOUT: - if (state->curr_screen >= SCREEN_RESULT) { + if (state->curr_screen >= WORDLE_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) + if (state->curr_screen != WORDLE_SCREEN_TITLE) display_title(state); break; - default: + case EVENT_MODE_LONG_PRESS: + if (state->curr_screen >= WORDLE_SCREEN_PLAYING) { + _activate(state); + } else { + movement_move_to_face(0); + } + break; + default: return movement_default_loop_handler(event); } return true; @@ -596,4 +646,3 @@ bool wordle_face_loop(movement_event_t event, void *context) { void wordle_face_resign(void *context) { (void) context; } - diff --git a/legacy/watch_faces/complication/wordle_face.h b/watch-faces/complication/wordle_face.h similarity index 85% rename from legacy/watch_faces/complication/wordle_face.h rename to watch-faces/complication/wordle_face.h index 5cc2f09a..e4f7d0f9 100644 --- a/legacy/watch_faces/complication/wordle_face.h +++ b/watch-faces/complication/wordle_face.h @@ -83,7 +83,15 @@ * 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 +#define WORDLE_USE_RANDOM_GUESS 3 +#define WORDLE_FREQ 2 +// To avoid a button press immedietly skipping a screen, we wait this many ticks +#define WORDLE_TICKS_RESULT 4 +#define WORDLE_TICK_WIN_LOSE 2 +#define WORDLE_TICK_BAD_GUESS 0 + +// Store this many words in our list of words that were already used to avoid too much repetition of guesses +#define WORDLE_MAX_BETWEEN_REPEATS 50 #include "wordle_face_dict.h" #define WORDLE_NUM_WORDS (sizeof(_valid_words) / sizeof(_valid_words[0])) @@ -95,28 +103,34 @@ typedef enum { WORDLE_LETTER_WRONG_LOC, WORDLE_LETTER_CORRECT, WORDLE_LETTER_COUNT -} WordleLetterResult; +} wordle_letter_result; typedef enum { - SCREEN_TITLE = 0, - SCREEN_STREAK, - SCREEN_CONTINUE, + WORDLE_SCREEN_TITLE = 0, + WORDLE_SCREEN_STREAK, + WORDLE_SCREEN_CONTINUE, #if WORDLE_USE_DAILY_STREAK - SCREEN_WAIT, + WORDLE_SCREEN_WAIT, #endif - SCREEN_PLAYING, - SCREEN_RESULT, - SCREEN_WIN, - SCREEN_LOSE, - SCREEN_NO_DICT, - SCREEN_ALREADY_GUESSED, - SCREEN_COUNT -} WordleScreen; + WORDLE_SCREEN_PLAYING, + WORDLE_SCREEN_RESULT, + WORDLE_SCREEN_WIN, + WORDLE_SCREEN_LOSE, + WORDLE_SCREEN_NO_DICT, + WORDLE_SCREEN_ALREADY_GUESSED, + WORDLE_SCREEN_COUNT +} wordle_screen; + +typedef enum { + BTN_MODE = 0, + BTN_ALARM, + BTN_LIGHT, +} wordle_pin_enum; typedef struct { // Anything you need to keep track of, put it here! uint8_t word_elements[WORDLE_LENGTH]; - WordleLetterResult word_elements_result[WORDLE_LENGTH]; + wordle_letter_result word_elements_result[WORDLE_LENGTH]; #if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES uint16_t guessed_words[WORDLE_MAX_ATTEMPTS]; #endif @@ -127,9 +141,12 @@ typedef struct { bool continuing : 1; bool skip_wrong_letter : 1; uint8_t streak; - WordleScreen curr_screen; + wordle_screen curr_screen; bool known_wrong_letters[WORDLE_NUM_VALID_LETTERS]; uint32_t day_last_game_started; + uint8_t ignore_btn_ticks; + uint16_t not_to_use[WORDLE_MAX_BETWEEN_REPEATS]; + uint8_t not_to_use_position; } wordle_state_t; void wordle_face_setup(uint8_t watch_face_index, void ** context_ptr); @@ -146,4 +163,3 @@ void wordle_face_resign(void *context); }) #endif // WORDLE_FACE_H_ - diff --git a/legacy/watch_faces/complication/wordle_face_dict.h b/watch-faces/complication/wordle_face_dict.h similarity index 100% rename from legacy/watch_faces/complication/wordle_face_dict.h rename to watch-faces/complication/wordle_face_dict.h diff --git a/watch-faces/sensor/activity_logging_face.c b/watch-faces/sensor/activity_logging_face.c index 3d5eb1cf..65d717f1 100644 --- a/watch-faces/sensor/activity_logging_face.c +++ b/watch-faces/sensor/activity_logging_face.c @@ -86,6 +86,13 @@ void activity_logging_face_activate(void *context) { bool activity_logging_face_loop(movement_event_t event, void *context) { activity_logging_state_t *state = (activity_logging_state_t *)context; switch (event.event_type) { + case EVENT_LIGHT_LONG_PRESS: + movement_illuminate_led(); + break; + case EVENT_LIGHT_BUTTON_DOWN: + state->display_index = (state->display_index + ACTIVITY_LOGGING_NUM_DAYS - 1) % ACTIVITY_LOGGING_NUM_DAYS; + _activity_logging_face_update_display(state); + break; case EVENT_ALARM_BUTTON_DOWN: state->display_index = (state->display_index + 1) % ACTIVITY_LOGGING_NUM_DAYS; // fall through diff --git a/watch-faces/sensor/activity_logging_face.h b/watch-faces/sensor/activity_logging_face.h index 6c5b74fb..f4f58b6e 100644 --- a/watch-faces/sensor/activity_logging_face.h +++ b/watch-faces/sensor/activity_logging_face.h @@ -40,6 +40,8 @@ * * A short press of the Alarm button moves backwards in the data log, showing yesterday's active minutes, * then the day before, etc. going back 14 days. + * A short press of the Light button moves forward in the data log, looping around if we're on the most-recent day. + * Holding the Light button will illuminate the display. * */ diff --git a/watch-faces/sensor/lis2dw_monitor_face.c b/watch-faces/sensor/lis2dw_monitor_face.c new file mode 100644 index 00000000..a717fe46 --- /dev/null +++ b/watch-faces/sensor/lis2dw_monitor_face.c @@ -0,0 +1,612 @@ +/* + * MIT License + * + * Copyright (c) 2025 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. + */ + +#include +#include +#include "lis2dw_monitor_face.h" +#include "watch.h" +#include "watch_utility.h" + +/* Display frequency */ +#define DISPLAY_FREQUENCY 8 + +/* Settings */ +#define NUM_SETTINGS 7 + +static void _settings_title_display(lis2dw_monitor_state_t *state, char *buf1, char *buf2) +{ + char buf[10]; + watch_display_text_with_fallback(WATCH_POSITION_TOP, buf1, buf2); + if (watch_get_lcd_type() != WATCH_LCD_TYPE_CUSTOM) { + snprintf(buf, sizeof(buf), "%2d", state->settings_page + 1); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + } +} + +static bool _settings_blink(uint8_t subsecond) +{ + if (subsecond % 2 == 0) { + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " ", " "); + return true; + } + return false; +} + +static void _settings_mode_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "MODE ", "MO"); + if (_settings_blink(subsecond)) + return; + + switch (state->ds.mode) { + case LIS2DW_MODE_LOW_POWER: + snprintf(buf, sizeof(buf), " LO "); + break; + case LIS2DW_MODE_HIGH_PERFORMANCE: + snprintf(buf, sizeof(buf), " HI "); + break; + case LIS2DW_MODE_ON_DEMAND: + snprintf(buf, sizeof(buf), " OD "); + break; + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_mode_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->ds.mode) { + case LIS2DW_MODE_LOW_POWER: + state->ds.mode = LIS2DW_MODE_HIGH_PERFORMANCE; + break; + case LIS2DW_MODE_HIGH_PERFORMANCE: + state->ds.mode = LIS2DW_MODE_ON_DEMAND; + break; + case LIS2DW_MODE_ON_DEMAND: + state->ds.mode = LIS2DW_MODE_LOW_POWER; + break; + } +} + +static void _settings_data_rate_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "RATE ", "DR"); + if (_settings_blink(subsecond)) + return; + + switch (state->ds.data_rate) { + case LIS2DW_DATA_RATE_POWERDOWN: + snprintf(buf, sizeof(buf), " -- "); + break; + case LIS2DW_DATA_RATE_LOWEST: + snprintf(buf, sizeof(buf), " LO "); + break; + case LIS2DW_DATA_RATE_12_5_HZ: + snprintf(buf, sizeof(buf), " 12Hz"); + break; + case LIS2DW_DATA_RATE_25_HZ: + snprintf(buf, sizeof(buf), " 25Hz"); + break; + default: + snprintf(buf, sizeof(buf), " HI "); + break; + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_data_rate_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->ds.data_rate) { + case LIS2DW_DATA_RATE_POWERDOWN: + state->ds.data_rate = LIS2DW_DATA_RATE_LOWEST; + break; + case LIS2DW_DATA_RATE_LOWEST: + state->ds.data_rate = LIS2DW_DATA_RATE_12_5_HZ; + break; + case LIS2DW_DATA_RATE_12_5_HZ: + state->ds.data_rate = LIS2DW_DATA_RATE_25_HZ; + break; + case LIS2DW_DATA_RATE_25_HZ: + state->ds.data_rate = LIS2DW_DATA_RATE_POWERDOWN; + break; + default: + state->ds.data_rate = LIS2DW_DATA_RATE_POWERDOWN; + break; + } +} + +static void _settings_low_power_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "LO PM", "LP"); + if (_settings_blink(subsecond)) + return; + + switch (state->ds.low_power) { + case LIS2DW_LP_MODE_1: + snprintf(buf, sizeof(buf), " L1 12"); + break; + case LIS2DW_LP_MODE_2: + snprintf(buf, sizeof(buf), " L2 14"); + break; + case LIS2DW_LP_MODE_3: + snprintf(buf, sizeof(buf), " L3 14"); + break; + case LIS2DW_LP_MODE_4: + snprintf(buf, sizeof(buf), " L4 14"); + break; + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_low_power_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->ds.low_power) { + case LIS2DW_LP_MODE_1: + state->ds.low_power = LIS2DW_LP_MODE_2; + break; + case LIS2DW_LP_MODE_2: + state->ds.low_power = LIS2DW_LP_MODE_3; + break; + case LIS2DW_LP_MODE_3: + state->ds.low_power = LIS2DW_LP_MODE_4; + break; + case LIS2DW_LP_MODE_4: + state->ds.low_power = LIS2DW_LP_MODE_1; + break; + } +} + +static void _settings_bwf_mode_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "BWF ", "BW"); + if (_settings_blink(subsecond)) + return; + + switch (state->ds.bwf_mode) { + case LIS2DW_BANDWIDTH_FILTER_DIV2: + snprintf(buf, sizeof(buf), " 2 "); + break; + case LIS2DW_BANDWIDTH_FILTER_DIV4: + snprintf(buf, sizeof(buf), " 4 "); + break; + case LIS2DW_BANDWIDTH_FILTER_DIV10: + snprintf(buf, sizeof(buf), " 10 "); + break; + case LIS2DW_BANDWIDTH_FILTER_DIV20: + snprintf(buf, sizeof(buf), " 20 "); + break; + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_bwf_mode_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->ds.bwf_mode) { + case LIS2DW_BANDWIDTH_FILTER_DIV2: + state->ds.bwf_mode = LIS2DW_BANDWIDTH_FILTER_DIV4; + break; + case LIS2DW_BANDWIDTH_FILTER_DIV4: + state->ds.bwf_mode = LIS2DW_BANDWIDTH_FILTER_DIV10; + break; + case LIS2DW_BANDWIDTH_FILTER_DIV10: + state->ds.bwf_mode = LIS2DW_BANDWIDTH_FILTER_DIV20; + break; + case LIS2DW_BANDWIDTH_FILTER_DIV20: + state->ds.bwf_mode = LIS2DW_BANDWIDTH_FILTER_DIV2; + break; + } +} + +static void _settings_range_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "RANGE", "RA"); + if (_settings_blink(subsecond)) + return; + + switch (state->ds.range) { + case LIS2DW_RANGE_2_G: + snprintf(buf, sizeof(buf), " 2g "); + break; + case LIS2DW_RANGE_4_G: + snprintf(buf, sizeof(buf), " 4g "); + break; + case LIS2DW_RANGE_8_G: + snprintf(buf, sizeof(buf), " 8g "); + break; + case LIS2DW_RANGE_16_G: + snprintf(buf, sizeof(buf), " 16g "); + break; + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_range_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->ds.range) { + case LIS2DW_RANGE_2_G: + state->ds.range = LIS2DW_RANGE_4_G; + break; + case LIS2DW_RANGE_4_G: + state->ds.range = LIS2DW_RANGE_8_G; + break; + case LIS2DW_RANGE_8_G: + state->ds.range = LIS2DW_RANGE_16_G; + break; + case LIS2DW_RANGE_16_G: + state->ds.range = LIS2DW_RANGE_2_G; + break; + } +} + +static void _settings_filter_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "FLT ", "FL"); + if (_settings_blink(subsecond)) + return; + + switch (state->ds.filter) { + case LIS2DW_FILTER_LOW_PASS: + snprintf(buf, sizeof(buf), " LP "); + break; + case LIS2DW_FILTER_HIGH_PASS: + snprintf(buf, sizeof(buf), " HP "); + break; + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_filter_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->ds.filter) { + case LIS2DW_FILTER_LOW_PASS: + state->ds.filter = LIS2DW_FILTER_HIGH_PASS; + break; + case LIS2DW_FILTER_HIGH_PASS: + state->ds.filter = LIS2DW_FILTER_LOW_PASS; + break; + } +} + +static void _settings_low_noise_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "LO NO", "LN"); + if (_settings_blink(subsecond)) + return; + + snprintf(buf, sizeof(buf), " %3s ", state->ds.low_noise ? "ON" : "OFF"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_low_noise_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + state->ds.low_noise = !state->ds.low_noise; +} + +/* Play beep sound */ +static inline void _beep() +{ + if (!movement_button_should_sound()) + return; + watch_buzzer_play_note(BUZZER_NOTE_C7, 50); +} + +/* Print lis2dw status to console. */ +static void _lis2dw_print_state(lis2dw_device_state_t *ds) +{ + printf("LIS2DW status:\n"); + printf(" Power mode:\t%x\n", ds->mode); + printf(" Data rate:\t%x\n", ds->data_rate); + printf(" LP mode:\t%x\n", ds->low_power); + printf(" BW filter:\t%x\n", ds->bwf_mode); + printf(" Range:\t%x \n", ds->range); + printf(" Filter type:\t%x\n", ds->filter); + printf(" Low noise:\t%x\n", ds->low_noise); + printf("\n"); +} + +static void _lis2dw_get_state(lis2dw_device_state_t *ds) +{ + ds->mode = lis2dw_get_mode(); + ds->data_rate = lis2dw_get_data_rate(); + ds->low_power = lis2dw_get_low_power_mode(); + ds->bwf_mode = lis2dw_get_bandwidth_filtering(); + ds->range = lis2dw_get_range(); + ds->filter = lis2dw_get_filter_type(); + ds->low_noise = lis2dw_get_low_noise_mode(); +} + +static void _lis2dw_set_state(lis2dw_device_state_t *ds) +{ + lis2dw_set_mode(ds->mode); + lis2dw_set_data_rate(ds->data_rate); + lis2dw_set_low_power_mode(ds->low_power); + lis2dw_set_bandwidth_filtering(ds->bwf_mode); + lis2dw_set_range(ds->range); + lis2dw_set_filter_type(ds->filter); + lis2dw_set_low_noise_mode(ds->low_noise); + + /* Additionally, set the background rate to the data rate. */ + movement_set_accelerometer_background_rate(ds->data_rate); +} + +static void _monitor_display(lis2dw_monitor_state_t *state) +{ + char buf[10]; + + snprintf(buf, sizeof(buf), " %C ", "XYZ"[state->axis]); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, buf, buf); + + snprintf(buf, sizeof(buf), "%2d", state->axis + 1); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + + if (state->show_title) { + snprintf(buf, sizeof(buf), "LIS2DW"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); + return; + } + + if (state->ds.data_rate == LIS2DW_DATA_RATE_POWERDOWN) { + /* No measurements available. */ + snprintf(buf, sizeof(buf), " -- "); + } else if (state->axis == 0) { + char sign = (state->reading.x) >= 0 ? ' ' : '-'; + snprintf(buf, sizeof(buf), "%c%.5d", sign, abs(state->reading.x)); + } else if (state->axis == 1) { + char sign = (state->reading.y) >= 0 ? ' ' : '-'; + snprintf(buf, sizeof(buf), "%c%.5d", sign, abs(state->reading.y)); + } else { + char sign = (state->reading.z) >= 0 ? ' ' : '-'; + snprintf(buf, sizeof(buf), "%c%.5d", sign, abs(state->reading.z)); + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _monitor_update(lis2dw_monitor_state_t *state) +{ + lis2dw_fifo_t fifo; + float x = 0, y = 0, z = 0; + + lis2dw_read_fifo(&fifo); + if (fifo.count == 0) { + return; + } + + /* Add up samples in fifo */ + for (uint8_t i = 0; i < fifo.count; i++) { + x += fifo.readings[i].x; + y += fifo.readings[i].y; + z += fifo.readings[i].z; + } + + /* Divide by number of samples */ + state->reading.x = (int16_t) (x / fifo.count); + state->reading.y = (int16_t) (y / fifo.count); + state->reading.z = (int16_t) (z / fifo.count); + + lis2dw_clear_fifo(); +} + +static void _switch_to_monitor(lis2dw_monitor_state_t *state) +{ + /* Switch to recording page */ + movement_request_tick_frequency(DISPLAY_FREQUENCY); + state->page = PAGE_LIS2DW_MONITOR; + state->show_title = DISPLAY_FREQUENCY; + _monitor_display(state); +} + +static void _switch_to_settings(lis2dw_monitor_state_t *state) +{ + /* Switch to chirping page */ + movement_request_tick_frequency(4); + state->page = PAGE_LIS2DW_SETTINGS; + state->settings_page = 0; + state->settings[state->settings_page].display(state, 0); +} + +static bool _monitor_loop(movement_event_t event, void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + watch_clear_colon(); + _monitor_update(state); + _monitor_display(state); + break; + case EVENT_TICK: + _monitor_update(state); + _monitor_display(state); + state->show_title = (state->show_title > 0) ? state->show_title - 1 : 0; + break; + case EVENT_ALARM_BUTTON_UP: + state->axis = (state->axis + 1) % 3; + _monitor_display(state); + break; + case EVENT_LIGHT_BUTTON_DOWN: + /* Do nothing. */ + break; + case EVENT_LIGHT_LONG_PRESS: + _switch_to_settings(state); + _beep(); + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +static bool _settings_loop(movement_event_t event, void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + case EVENT_TICK: + state->settings[state->settings_page].display(context, event.subsecond); + break; + case EVENT_LIGHT_BUTTON_UP: + state->settings_page = (state->settings_page + 1) % NUM_SETTINGS; + state->settings[state->settings_page].display(context, event.subsecond); + break; + case EVENT_MODE_BUTTON_UP: + _lis2dw_set_state(&state->ds); + _lis2dw_print_state(&state->ds); + _switch_to_monitor(state); + _beep(); + break; + case EVENT_LIGHT_BUTTON_DOWN: + /* Do nothing. */ + break; + case EVENT_ALARM_BUTTON_UP: + /* Advance current settings */ + state->settings[state->settings_page].advance(context); + state->settings[state->settings_page].display(context, event.subsecond); + break; + default: + _lis2dw_set_state(&state->ds); + movement_default_loop_handler(event); + break; + } + return true; +} + +void lis2dw_monitor_face_setup(uint8_t watch_face_index, void **context_ptr) +{ + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(lis2dw_monitor_state_t)); + memset(*context_ptr, 0, sizeof(lis2dw_monitor_state_t)); + } + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) * context_ptr; + + /* Default setup */ + state->axis = 0; + + /* Initialize settings */ + uint8_t settings_page = 0; + state->settings = malloc(NUM_SETTINGS * sizeof(lis2dw_settings_t)); + state->settings[settings_page].display = _settings_mode_display; + state->settings[settings_page].advance = _settings_mode_advance; + settings_page++; + state->settings[settings_page].display = _settings_data_rate_display; + state->settings[settings_page].advance = _settings_data_rate_advance; + settings_page++; + state->settings[settings_page].display = _settings_low_power_display; + state->settings[settings_page].advance = _settings_low_power_advance; + settings_page++; + state->settings[settings_page].display = _settings_bwf_mode_display; + state->settings[settings_page].advance = _settings_bwf_mode_advance; + settings_page++; + state->settings[settings_page].display = _settings_range_display; + state->settings[settings_page].advance = _settings_range_advance; + settings_page++; + state->settings[settings_page].display = _settings_filter_display; + state->settings[settings_page].advance = _settings_filter_advance; + settings_page++; + state->settings[settings_page].display = _settings_low_noise_display; + state->settings[settings_page].advance = _settings_low_noise_advance; + settings_page++; +} + +void lis2dw_monitor_face_activate(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + /* Setup lis2dw to run in background at 12.5 Hz sampling rate. */ + movement_set_accelerometer_background_rate(LIS2DW_DATA_RATE_12_5_HZ); + + /* Enable fifo and clear it. */ + lis2dw_enable_fifo(); + lis2dw_clear_fifo(); + + /* Print lis2dw status to console. */ + _lis2dw_get_state(&state->ds); + _lis2dw_print_state(&state->ds); + + /* Switch to monitor page. */ + _switch_to_monitor(state); +} + +bool lis2dw_monitor_face_loop(movement_event_t event, void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->page) { + default: + case PAGE_LIS2DW_MONITOR: + return _monitor_loop(event, context); + case PAGE_LIS2DW_SETTINGS: + return _settings_loop(event, context); + } +} + +void lis2dw_monitor_face_resign(void *context) +{ + (void) context; + lis2dw_clear_fifo(); + lis2dw_disable_fifo(); +} + +movement_watch_face_advisory_t lis2dw_monitor_face_advise(void *context) +{ + (void) context; + movement_watch_face_advisory_t retval = { 0 }; + return retval; +} diff --git a/watch-faces/sensor/lis2dw_monitor_face.h b/watch-faces/sensor/lis2dw_monitor_face.h new file mode 100644 index 00000000..51a98ecf --- /dev/null +++ b/watch-faces/sensor/lis2dw_monitor_face.h @@ -0,0 +1,81 @@ +/* + * MIT License + * + * Copyright (c) 2025 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. + */ + +/* + * This watch face displays the current reading of the LIS2DW12 accelerometer. + * The axis (x,y,z) can be selected using the alarm button. + * + * A long press on the light button allows to configure the sensor, including + * its mode, data rate, low power mode, bandwidth filtering, range, filter type, + * and low noise mode. + * + * The watch face is mainly designed for experimenting with the sensor and + * configuring it for other developing other watch faces. + */ + +#include "movement.h" + +typedef enum { + PAGE_LIS2DW_MONITOR, + PAGE_LIS2DW_SETTINGS, +} lis2dw_monitor_page_t; + +typedef struct { + lis2dw_mode_t mode; + lis2dw_data_rate_t data_rate; + lis2dw_low_power_mode_t low_power; + lis2dw_bandwidth_filtering_mode_t bwf_mode; + lis2dw_range_t range; + lis2dw_filter_t filter; + bool low_noise; +} lis2dw_device_state_t; + +typedef struct { + void (*display)(void *, uint8_t); + void (*advance)(void *); +} lis2dw_settings_t; + +typedef struct { + uint8_t axis:2; /* Axis to display */ + lis2dw_reading_t reading; /* Current reading */ + lis2dw_monitor_page_t page; /* Displayed page */ + lis2dw_device_state_t ds; /* Device state */ + uint8_t settings_page:3; /* Subpage in settings */ + lis2dw_settings_t *settings; /* Settings config */ + uint8_t show_title:6; /* Display face title */ +} lis2dw_monitor_state_t; + +void lis2dw_monitor_face_setup(uint8_t watch_face_index, void **context_ptr); +void lis2dw_monitor_face_activate(void *context); +bool lis2dw_monitor_face_loop(movement_event_t event, void *context); +void lis2dw_monitor_face_resign(void *context); +movement_watch_face_advisory_t lis2dw_monitor_face_advise(void *context); + +#define lis2dw_monitor_face ((const watch_face_t){ \ + lis2dw_monitor_face_setup, \ + lis2dw_monitor_face_activate, \ + lis2dw_monitor_face_loop, \ + lis2dw_monitor_face_resign, \ + lis2dw_monitor_face_advise, \ +}) diff --git a/watch-faces/settings/set_time_face.c b/watch-faces/settings/set_time_face.c index 1bcf2123..e273e61d 100644 --- a/watch-faces/settings/set_time_face.c +++ b/watch-faces/settings/set_time_face.c @@ -44,9 +44,9 @@ static void _handle_alarm_button(watch_date_time_t date_time, uint8_t current_pa movement_set_timezone_index(movement_get_timezone_index() + 1); if (movement_get_timezone_index() >= NUM_ZONE_NAMES) movement_set_timezone_index(0); current_offset = movement_get_current_timezone_offset_for_zone(movement_get_timezone_index()); - break; + return; case 0: // year - date_time.unit.year = ((date_time.unit.year % 60) + 1); + date_time.unit.year = (date_time.unit.year + 1) % 60; break; case 1: // month date_time.unit.month = (date_time.unit.month % 12) + 1; @@ -91,6 +91,8 @@ bool set_time_face_loop(movement_event_t event, void *context) { watch_date_time_t date_time = movement_get_local_date_time(); switch (event.event_type) { + case EVENT_ACTIVATE: + break; case EVENT_TICK: if (_quick_ticks_running) { if (HAL_GPIO_BTN_ALARM_read()) _handle_alarm_button(date_time, current_page); diff --git a/watch-library/hardware/watch/watch_i2c.c b/watch-library/hardware/watch/watch_i2c.c index 709c63b7..1a89f80c 100644 --- a/watch-library/hardware/watch/watch_i2c.c +++ b/watch-library/hardware/watch/watch_i2c.c @@ -38,27 +38,31 @@ void watch_disable_i2c(void) { i2c_disable(); } -void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { - i2c_write(addr, buf, length); +int8_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { + return (int8_t)i2c_write(addr, buf, length); } -void watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { - i2c_read(addr, buf, length); +int8_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { + return (int8_t)i2c_read(addr, buf, length); } -void watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { +int8_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { uint8_t buf[2]; buf[0] = reg; buf[1] = data; - watch_i2c_send(addr, (uint8_t *)&buf, 2); + return (int8_t)watch_i2c_send(addr, (uint8_t *)&buf, 2); } uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) { uint8_t data; - watch_i2c_send(addr, (uint8_t *)®, 1); - watch_i2c_receive(addr, (uint8_t *)&data, 1); + if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) { + return 0; + } + if (watch_i2c_receive(addr, (uint8_t *)&data, 1) != 0) { + return 0; + } return data; } @@ -66,9 +70,12 @@ uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) { uint16_t watch_i2c_read16(int16_t addr, uint8_t reg) { uint16_t data; - watch_i2c_send(addr, (uint8_t *)®, 1); - watch_i2c_receive(addr, (uint8_t *)&data, 2); - + if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) { + return 0; + } + if (watch_i2c_receive(addr, (uint8_t *)&data, 2) != 0) { + return 0; + } return data; } @@ -76,18 +83,24 @@ uint32_t watch_i2c_read24(int16_t addr, uint8_t reg) { uint32_t data; data = 0; - watch_i2c_send(addr, (uint8_t *)®, 1); - watch_i2c_receive(addr, (uint8_t *)&data, 3); - + if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) { + return 0; + } + if (watch_i2c_receive(addr, (uint8_t *)&data, 3) != 0) { + return 0; + } return data << 8; } uint32_t watch_i2c_read32(int16_t addr, uint8_t reg) { uint32_t data; - watch_i2c_send(addr, (uint8_t *)®, 1); - watch_i2c_receive(addr, (uint8_t *)&data, 4); - + if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) { + return 0; + } + if (watch_i2c_receive(addr, (uint8_t *)&data, 4) != 0) { + return 0; + } return data; } diff --git a/watch-library/hardware/watch/watch_slcd.c b/watch-library/hardware/watch/watch_slcd.c index db6b1942..337dd848 100644 --- a/watch-library/hardware/watch/watch_slcd.c +++ b/watch-library/hardware/watch/watch_slcd.c @@ -252,7 +252,7 @@ void watch_enable_display(void) { slcd_clear(); if (_installed_display == WATCH_LCD_TYPE_CUSTOM) { - slcd_set_contrast(4); + slcd_set_contrast(0); } else { slcd_set_contrast(9); } @@ -260,6 +260,10 @@ void watch_enable_display(void) { slcd_enable(); } +void watch_disable_display(void) { + slcd_disable(); +} + inline void watch_set_pixel(uint8_t com, uint8_t seg) { slcd_set_segment(com, seg); } diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index b716f790..1c89f244 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -302,6 +302,20 @@ void lis2dw_clear_fifo(void) { #endif } +void lis2dw_enable_double_tap(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS, configuration | LIS2DW_WAKE_UP_THS_ENABLE_DOUBLE_TAP); +#endif +} + +void lis2dw_disable_double_tap(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS, configuration & ~LIS2DW_WAKE_UP_THS_ENABLE_DOUBLE_TAP); +#endif +} + void lis2dw_enable_sleep(void) { #ifdef I2C_SERCOM uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index 57ff05b2..3b017c65 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -343,6 +343,10 @@ bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data); void lis2dw_clear_fifo(void); +void lis2dw_enable_double_tap(void); + +void lis2dw_disable_double_tap(void); + void lis2dw_enable_sleep(void); void lis2dw_disable_sleep(void); diff --git a/watch-library/shared/watch/watch_common_display.c b/watch-library/shared/watch/watch_common_display.c index ed558a1b..86ab352d 100644 --- a/watch-library/shared/watch/watch_common_display.c +++ b/watch-library/shared/watch/watch_common_display.c @@ -57,6 +57,7 @@ void watch_display_character(uint8_t character, uint8_t position) { else if (character == 'c') character = 'C'; // C needs to be uppercase else if (character == 'J') character = 'j'; // same else if (character == 'v' || character == 'V' || character == 'U' || character == 'W' || character == 'w') character = 'u'; // bottom segment duplicated, so show in top half + else if (character == 't' || character == 'T') character = '+'; // avoid confusion with uppercase E } else { if (character == 'u') character = 'v'; // we can use the bottom segment; move to lower half else if (character == 'j') character = 'J'; // same but just display a normal J diff --git a/watch-library/shared/watch/watch_i2c.h b/watch-library/shared/watch/watch_i2c.h index fbcc1a92..18d07b38 100644 --- a/watch-library/shared/watch/watch_i2c.h +++ b/watch-library/shared/watch/watch_i2c.h @@ -45,22 +45,25 @@ void watch_disable_i2c(void); * @param addr The address of the device you wish to talk to. * @param buf A series of unsigned bytes; the data you wish to transmit. * @param length The number of bytes in buf that you wish to send. + * @return 0 if no error code, otherwise a code via i2c_result_t */ -void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length); +int8_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length); /** @brief Receives a series of values from a device on the I2C bus. * @param addr The address of the device you wish to hear from. * @param buf Storage for the incoming bytes; on return, it will contain the received data. * @param length The number of bytes that you wish to receive. + * @return 0 if no error code, otherwise a code via i2c_result_t */ -void watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length); +int8_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length); /** @brief Writes a byte to a register in an I2C device. * @param addr The address of the device you wish to address. * @param reg The register on the device that you wish to set. * @param data The value that you wish to set the register to. + * @return 0 if no error code, otherwise a code via i2c_result_t */ -void watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data); +int8_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data); /** @brief Reads a byte from a register in an I2C device. * @param addr The address of the device you wish to address. diff --git a/watch-library/shared/watch/watch_slcd.h b/watch-library/shared/watch/watch_slcd.h index b94b3f80..43a4f849 100644 --- a/watch-library/shared/watch/watch_slcd.h +++ b/watch-library/shared/watch/watch_slcd.h @@ -98,6 +98,10 @@ typedef enum { */ void watch_enable_display(void); +/** @brief Disables the Segment LCD display. + */ +void watch_disable_display(void); + /** @brief Sets a pixel. Use this to manually set a pixel with a given common and segment number. * See segmap.html. * @param com the common pin, numbered from 0-2. diff --git a/watch-library/shared/watch/watch_utility.c b/watch-library/shared/watch/watch_utility.c index 6d024032..7179aa72 100644 --- a/watch-library/shared/watch/watch_utility.c +++ b/watch-library/shared/watch/watch_utility.c @@ -205,7 +205,8 @@ uint32_t watch_utility_date_time_to_unix_time(watch_date_time_t date_time, int32 watch_date_time_t watch_utility_date_time_from_unix_time(uint32_t timestamp, int32_t utc_offset) { watch_date_time_t retval; retval.reg = 0; - int32_t days, secs; + uint32_t secs; + int32_t days; int32_t remdays, remsecs, remyears; int32_t qc_cycles, c_cycles, q_cycles; int32_t years, months; diff --git a/watch-library/simulator/shell.html b/watch-library/simulator/shell.html index 8306b919..51d289cb 100644 --- a/watch-library/simulator/shell.html +++ b/watch-library/simulator/shell.html @@ -64,22 +64,22 @@

Sensor Watch Emulator

- - - - - - - + + + + + + + - - - - + + + diff --git a/watch-library/simulator/watch/watch_deepsleep.c b/watch-library/simulator/watch/watch_deepsleep.c index 1bc4be73..88ac2da9 100644 --- a/watch-library/simulator/watch/watch_deepsleep.c +++ b/watch-library/simulator/watch/watch_deepsleep.c @@ -25,17 +25,37 @@ #include #include "watch_extint.h" #include "app.h" +#include + static uint32_t watch_backup_data[8]; +static bool _wake_up = false; +static watch_cb_t _callback = NULL; + + +void _wake_up_simulator(void) { + _wake_up = true; +} + +static void cb_extwake_wrapper(void) { + _wake_up_simulator(); + + if (_callback) { + _callback(); + } +} + void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool level) { if (pin == HAL_GPIO_BTN_ALARM_pin()) { + _callback = callback; watch_enable_external_interrupts(); - watch_register_interrupt_callback(pin, callback, level ? INTERRUPT_TRIGGER_RISING : INTERRUPT_TRIGGER_FALLING); + watch_register_interrupt_callback(pin, cb_extwake_wrapper, level ? INTERRUPT_TRIGGER_RISING : INTERRUPT_TRIGGER_FALLING); } } void watch_disable_extwake_interrupt(uint8_t pin) { if (pin == HAL_GPIO_BTN_ALARM_pin()) { + _callback = NULL; watch_register_interrupt_callback(pin, NULL, INTERRUPT_TRIGGER_NONE); } } @@ -57,23 +77,33 @@ uint32_t watch_get_backup_data(uint8_t reg) { void watch_enter_sleep_mode(void) { // TODO: (a2) hook to UI - // enter standby (4); we basically hang out here until an interrupt wakes us. - // sleep(4); + // disable tick interrupt + watch_rtc_disable_all_periodic_callbacks(); + + // // disable all buttons but alarm + watch_register_interrupt_callback(HAL_GPIO_BTN_MODE_pin(), NULL, INTERRUPT_TRIGGER_NONE); + watch_register_interrupt_callback(HAL_GPIO_BTN_LIGHT_pin(), NULL, INTERRUPT_TRIGGER_NONE); + + sleep(4); // call app_setup so the app can re-enable everything we disabled. app_setup(); } -void watch_enter_deep_sleep_mode(void) { - // identical to sleep mode except we disable the LCD first. - // TODO: (a2) hook to UI - - watch_enter_sleep_mode(); -} - void watch_enter_backup_mode(void) { // TODO: (a2) hook to UI // go into backup sleep mode (5). when we exit, the reset controller will take over. - // sleep(5); + sleep(5); +} + +void sleep(const uint8_t mode) { + (void) mode; + + // we basically hang out here until an interrupt wakes us. + while(!_wake_up) { + emscripten_sleep(100); + } + + _wake_up = false; } diff --git a/watch-library/simulator/watch/watch_i2c.c b/watch-library/simulator/watch/watch_i2c.c index 09339888..8086fb75 100644 --- a/watch-library/simulator/watch/watch_i2c.c +++ b/watch-library/simulator/watch/watch_i2c.c @@ -28,11 +28,17 @@ void watch_enable_i2c(void) {} void watch_disable_i2c(void) {} -void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) {} +int8_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { + return 0; +} -void watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) {} +int8_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { + return 0; +} -void watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) {} +int8_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { + return 0; +} uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) { return 0; diff --git a/watch-library/simulator/watch/watch_slcd.c b/watch-library/simulator/watch/watch_slcd.c index 34d8878c..1990e90c 100644 --- a/watch-library/simulator/watch/watch_slcd.c +++ b/watch-library/simulator/watch/watch_slcd.c @@ -50,17 +50,23 @@ void watch_enable_display(void) { _watch_update_indicator_segments(); #endif - EM_ASM({ #if defined(FORCE_CUSTOM_LCD_TYPE) - document.getElementById("classic").style.display = "none"; + EM_ASM({document.getElementById("custom").style.display = "";}); + EM_ASM({document.getElementById("classic").style.display = "none";}); #else - document.getElementById("custom").style.display = "none"; + EM_ASM({document.getElementById("custom").style.display = "none";}); + EM_ASM({document.getElementById("classic").style.display = "";}); #endif - }); watch_clear_display(); } +void watch_disable_display(void) { + watch_clear_display(); + EM_ASM({document.getElementById("classic").style.display = "none";}); + EM_ASM({document.getElementById("custom").style.display = "none";}); +} + void watch_set_pixel(uint8_t com, uint8_t seg) { EM_ASM({ document.querySelectorAll("[data-com='" + $0 + "'][data-seg='" + $1 + "']") diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index f0046849..5466eebb 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -190,37 +190,29 @@ void watch_enable_leds(void) {} void watch_disable_leds(void) {} -void watch_set_led_color(uint8_t red, uint8_t green) { +void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { EM_ASM({ - // the watch svg contains an feColorMatrix filter with id ledcolor - // and a green svg gradient that mimics the led being on - // https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feColorMatrix - // this changes the color of the gradient to match the red+green combination let filter = document.getElementById("ledcolor"); let color_matrix = filter.children[0].values.baseVal; - color_matrix[1].value = $0 / 255; // red value - color_matrix[6].value = $1 / 255; // green value - document.getElementById('light').style.opacity = Math.min(255, $0 + $1) / 255; - }, red, green); -} - -void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { - (void) blue; - watch_set_led_color(red, green); + color_matrix[0].value = $0 / 255; // red + color_matrix[6].value = $1 / 255; // green + color_matrix[12].value = $2 / 255; // blue + document.getElementById('light').style.opacity = Math.min(255, $0 + $1 + $2) / 255; + }, red, green, blue); } void watch_set_led_red(void) { - watch_set_led_color(255, 0); + watch_set_led_color_rgb(255, 0, 0); } void watch_set_led_green(void) { - watch_set_led_color(0, 255); + watch_set_led_color_rgb(0, 255, 0); } void watch_set_led_yellow(void) { - watch_set_led_color(255, 255); + watch_set_led_color_rgb(255, 255, 0); } void watch_set_led_off(void) { - watch_set_led_color(0, 0); + watch_set_led_color_rgb(0, 0, 0); }