Merge pull request #1 from joeycastillo/main

Updating.
This commit is contained in:
Michael Ciuffo 2025-11-23 13:35:02 -08:00 committed by GitHub
commit 257dab38e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 5036 additions and 520 deletions

View File

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

View File

@ -28,7 +28,6 @@
#include "world_clock2_face.h"
#include "watch.h"
#include "watch_utility.h"
#include "watch_utility.h"
static bool refresh_face;

View File

@ -27,9 +27,7 @@
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "app.h"
#include "watch.h"
#include "watch_utility.h"
@ -51,11 +49,12 @@
#if __EMSCRIPTEN__
#include <emscripten.h>
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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, \
})

View File

@ -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 <time.h>
#endif
#include <stdlib.h>
#include <string.h>
#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; i<MAX_PLAYER_CARDS_DISPLAY; i++) {
card = player.hand[player.idx_hand - MAX_PLAYER_CARDS_DISPLAY + i];
display_card_at_position(card, BOARD_DISPLAY_START + i);
}
}
}
static void display_dealer_hand(void) {
uint8_t card = dealer.hand[dealer.idx_hand - 1];
display_card_at_position(card, 0);
}
static void display_score(uint8_t score, watch_position_t pos) {
char buf[4];
sprintf(buf, "%2d", score);
watch_display_text(pos, buf);
}
static void add_to_game_scores(bool add_to_wins) {
g_state->games_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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,6 +25,7 @@
#include <stdlib.h>
#include <string.h>
#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);
}

View File

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

View File

@ -30,19 +30,22 @@
#include <stdlib.h>
#include <string.h>
#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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,216 @@
/* SPDX-License-Identifier: MIT */
/*
* MIT License
*
* Copyright © 2021-2022 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com>
* Copyright © 2023 Jeremy O'Brien <neutral@fastmail.com>
* Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.com> (https://www.matheusmoreira.com/)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include <string.h>
#include "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;
}

View File

@ -0,0 +1,87 @@
/* SPDX-License-Identifier: MIT */
/*
* MIT License
*
* Copyright © 2021-2022 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com>
* Copyright © 2022 Alexsander Akers <me@a2.io>
* Copyright © 2023 Alex Utter <ooterness@gmail.com>
* Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.com> (https://www.matheusmoreira.com/)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* 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_

View File

@ -23,6 +23,7 @@
*/
#include "simon_face.h"
#include "delay.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -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);

View File

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

View File

@ -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 <stdlib.h>
#include <string.h>
#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; j<j_len; j++){
watch_set_pixel(pixels[i][j][0], pixels[i][j][1]);
}
delay_ms(150);
}
}
}
static void load_animation(void) {
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
int j_len = 2;
int pixels[3][4][2] = {
{
{0, 3},
{0, 6}
},
{
{1, 3},
{1, 5}
},
{
{2, 2},
{2, 4}
}
};
set_pixels(pixels, j_len);
} else{
int j_len = 4;
int pixels[3][4][2] = {
{
{2, 22},
{2, 15},
{1, 2},
{1, 4}
},
{
{0, 16},
{0, 15},
{0, 1},
{0, 3}
},
{
{3, 16},
{3, 14},
{3, 1},
{3, 3}
}
};
set_pixels(pixels, j_len);
}
}
static void _blink_face_update_lcd(simple_coin_flip_face_state_t *state) {
(void) state;
watch_clear_display();
load_animation();
watch_clear_display();
int r = get_random(2);
if(r){
watch_display_text(WATCH_POSITION_BOTTOM, "Heads");
}else{
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_display_text(WATCH_POSITION_BOTTOM, " Tails");
}else {
watch_display_text(WATCH_POSITION_BOTTOM, "Tails");
}
}
}
bool simple_coin_flip_face_loop(movement_event_t event, void *context) {
simple_coin_flip_face_state_t *state = (simple_coin_flip_face_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
draw_start_face();
break;
case EVENT_TICK:
if(!state->is_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;
}

View File

@ -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, \
})

View File

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

View File

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

View File

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

View File

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

View File

@ -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, \
})

View File

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

View File

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

View File

@ -26,6 +26,7 @@
#include <string.h>
#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;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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, \
})

View File

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

View File

@ -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 *)&reg, 1);
watch_i2c_receive(addr, (uint8_t *)&data, 1);
if (watch_i2c_send(addr, (uint8_t *)&reg, 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 *)&reg, 1);
watch_i2c_receive(addr, (uint8_t *)&data, 2);
if (watch_i2c_send(addr, (uint8_t *)&reg, 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 *)&reg, 1);
watch_i2c_receive(addr, (uint8_t *)&data, 3);
if (watch_i2c_send(addr, (uint8_t *)&reg, 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 *)&reg, 1);
watch_i2c_receive(addr, (uint8_t *)&data, 4);
if (watch_i2c_send(addr, (uint8_t *)&reg, 1) != 0) {
return 0;
}
if (watch_i2c_receive(addr, (uint8_t *)&data, 4) != 0) {
return 0;
}
return data;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <a href="segmap.html">segmap.html</a>.
* @param com the common pin, numbered from 0-2.

View File

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

View File

@ -64,22 +64,22 @@
<h1 style="text-align: center;">Sensor Watch Emulator</h1>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1271 1311" width="320">
<defs>
<radialGradient id="Dégradé_sans_nom_3" data-name="Dégradé sans nom 3" cx="635" cy="687" fx="260.1751499985994" r="374.83" gradientUnits="userSpaceOnUse">
<stop offset="0.28" stop-color="lime"/>
<stop offset="0.46" stop-color="#00b200" stop-opacity="0.7"/>
<stop offset="0.65" stop-color="#006700" stop-opacity="0.4"/>
<stop offset="0.82" stop-color="#002f00" stop-opacity="0.19"/>
<stop offset="0.94" stop-color="#000d00" stop-opacity="0.05"/>
<stop offset="1" stop-opacity="0"/>
<radialGradient id="Dégradé_sans_nom_3" data-name="Dégradé sans nom 3"
cx="635" cy="687" fx="260.1751499985994" r="374.83" gradientUnits="userSpaceOnUse">
<stop offset="0.28" stop-color="white"/>
<stop offset="0.46" stop-color="#ccc" stop-opacity="0.7"/>
<stop offset="0.65" stop-color="#999" stop-opacity="0.4"/>
<stop offset="0.82" stop-color="#666" stop-opacity="0.19"/>
<stop offset="0.94" stop-color="#333" stop-opacity="0.05"/>
<stop offset="1" stop-color="#000" stop-opacity="0"/>
</radialGradient>
<filter id="ledcolor">
<feColorMatrix in="SourceGraphic" type="matrix"
values=" 0 0 0 0 0
0 1 0 0 0
0 0 0 0 0
0 0 0 1 0 "/>
</filter>
<filter id="ledcolor">
<feColorMatrix in="SourceGraphic" type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 1 0"/>
</filter>
</defs>
<g id="Calque">
<path id="btn3" d="M1226,777h39.08a5.92,5.92,0,0,1,5.92,5.92v58.16a5.92,5.92,0,0,1-5.92,5.92H1226a0,0,0,0,1,0,0V777A0,0,0,0,1,1226,777Z" style="fill: #999"/>

View File

@ -25,17 +25,37 @@
#include <stddef.h>
#include "watch_extint.h"
#include "app.h"
#include <emscripten.h>
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;
}

View File

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

View File

@ -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 + "']")

View File

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