Merge branch 'main' into wordle_port
This commit is contained in:
commit
c2ac4a7e4c
2
Makefile
2
Makefile
@ -20,6 +20,8 @@ TINYUSB_CDC=1
|
||||
# Now we're all set to include gossamer's make rules.
|
||||
include $(GOSSAMER_PATH)/make.mk
|
||||
|
||||
CFLAGS+=-D_POSIX_C_SOURCE=200112L
|
||||
|
||||
define n
|
||||
|
||||
|
||||
|
||||
@ -28,7 +28,6 @@
|
||||
#include "world_clock2_face.h"
|
||||
#include "watch.h"
|
||||
#include "watch_utility.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
static bool refresh_face;
|
||||
|
||||
|
||||
26
movement.c
26
movement.c
@ -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};
|
||||
@ -520,7 +519,7 @@ bool movement_enable_tap_detection_if_available(void) {
|
||||
// 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);
|
||||
@ -587,6 +586,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 +601,7 @@ float movement_get_temperature(void) {
|
||||
val = val >> 4;
|
||||
temperature_c = 25 + (float)val / 16.0;
|
||||
}
|
||||
#endif
|
||||
|
||||
return temperature_c;
|
||||
}
|
||||
@ -609,13 +614,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 +950,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 +981,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 +1028,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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -59,8 +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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ 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 \
|
||||
@ -38,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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
439
watch-faces/complication/baby_kicks_face.c
Normal file
439
watch-faces/complication/baby_kicks_face.c
Normal 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;
|
||||
}
|
||||
132
watch-faces/complication/baby_kicks_face.h
Normal file
132
watch-faces/complication/baby_kicks_face.h
Normal 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, \
|
||||
})
|
||||
467
watch-faces/complication/blackjack_face.c
Executable file
467
watch-faces/complication/blackjack_face.c
Executable 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();
|
||||
}
|
||||
}
|
||||
91
watch-faces/complication/blackjack_face.h
Executable file
91
watch-faces/complication/blackjack_face.h
Executable 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_
|
||||
153
watch-faces/complication/counter_face.c
Normal file
153
watch-faces/complication/counter_face.c
Normal 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;
|
||||
}
|
||||
63
watch-faces/complication/counter_face.h
Normal file
63
watch-faces/complication/counter_face.h
Normal 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_
|
||||
637
watch-faces/complication/deadline_face.c
Normal file
637
watch-faces/complication/deadline_face.c
Normal 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;
|
||||
}
|
||||
65
watch-faces/complication/deadline_face.h
Normal file
65
watch-faces/complication/deadline_face.h
Normal 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_
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
@ -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.
|
||||
@ -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);
|
||||
577
watch-faces/complication/lander_face.c
Normal file
577
watch-faces/complication/lander_face.c
Normal 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 ( );
|
||||
}
|
||||
152
watch-faces/complication/lander_face.h
Normal file
152
watch-faces/complication/lander_face.h
Normal 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_
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
|
||||
216
watch-faces/complication/pulsometer_face.c
Normal file
216
watch-faces/complication/pulsometer_face.c
Normal 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;
|
||||
}
|
||||
87
watch-faces/complication/pulsometer_face.h
Normal file
87
watch-faces/complication/pulsometer_face.h
Normal 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_
|
||||
@ -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);
|
||||
@ -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, \
|
||||
180
watch-faces/complication/simple_coin_flip_face.c
Normal file
180
watch-faces/complication/simple_coin_flip_face.c
Normal 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;
|
||||
}
|
||||
|
||||
60
watch-faces/complication/simple_coin_flip_face.h
Normal file
60
watch-faces/complication/simple_coin_flip_face.h
Normal 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, \
|
||||
})
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
@ -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;
|
||||
}
|
||||
@ -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, \
|
||||
})
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
612
watch-faces/sensor/lis2dw_monitor_face.c
Normal file
612
watch-faces/sensor/lis2dw_monitor_face.c
Normal 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;
|
||||
}
|
||||
81
watch-faces/sensor/lis2dw_monitor_face.h
Normal file
81
watch-faces/sensor/lis2dw_monitor_face.h
Normal 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, \
|
||||
})
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -43,6 +43,8 @@ uint8_t IndicatorSegments[8] = {
|
||||
};
|
||||
|
||||
void watch_display_character(uint8_t character, uint8_t position) {
|
||||
if((character-0x20 < 0) | (character-0x20 >= sizeof(Classic_LCD_Character_Set)) return ;
|
||||
|
||||
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) {
|
||||
if (character == 'R' && position > 1 && position < 8) character = 'r'; // We can't display uppercase R in these positions
|
||||
else if (character == 'T' && position > 1 && position < 8) character = 't'; // lowercase t is the only option for these positions
|
||||
@ -57,6 +59,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
|
||||
@ -124,9 +127,9 @@ void watch_display_character(uint8_t character, uint8_t position) {
|
||||
|
||||
void watch_display_character_lp_seconds(uint8_t character, uint8_t position) {
|
||||
// Will only work for digits and for positions 8 and 9 - but less code & checks to reduce power consumption
|
||||
|
||||
digit_mapping_t segmap;
|
||||
uint8_t segdata;
|
||||
if(character < 20) return ;
|
||||
|
||||
/// TODO: See optimization note above.
|
||||
|
||||
@ -168,6 +171,7 @@ void watch_display_string(const char *string, uint8_t position) {
|
||||
}
|
||||
|
||||
void watch_display_text(watch_position_t location, const char *string) {
|
||||
if(!string[0]) return ;
|
||||
switch (location) {
|
||||
case WATCH_POSITION_TOP:
|
||||
case WATCH_POSITION_TOP_LEFT:
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user