diff --git a/Makefile b/Makefile index cae2b943..3c4436f3 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,9 @@ TINYUSB_CDC=1 # Now we're all set to include gossamer's make rules. include $(GOSSAMER_PATH)/make.mk +# Don't add gossamer's rtc.c since we are using our own rtc32.c +SRCS := $(filter-out $(GOSSAMER_PATH)/peripherals/rtc.c,$(SRCS)) + CFLAGS+=-D_POSIX_C_SOURCE=200112L define n @@ -136,6 +139,7 @@ INCLUDES += \ -I./watch-library/hardware/watch \ SRCS += \ + ./watch-library/hardware/watch/rtc32.c \ ./watch-library/hardware/watch/watch.c \ ./watch-library/hardware/watch/watch_adc.c \ ./watch-library/hardware/watch/watch_deepsleep.c \ diff --git a/legacy/watch_faces/clock/world_clock2_face.c b/legacy/watch_faces/clock/world_clock2_face.c index e79f2df7..e47bc44d 100644 --- a/legacy/watch_faces/clock/world_clock2_face.c +++ b/legacy/watch_faces/clock/world_clock2_face.c @@ -28,7 +28,6 @@ #include "world_clock2_face.h" #include "watch.h" #include "watch_utility.h" -#include "watch_utility.h" static bool refresh_face; diff --git a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c index 72584749..cb49e018 100644 --- a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c +++ b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c @@ -448,13 +448,13 @@ static void start_reading(accelerometer_data_acquisition_state_t *state) { state->records[state->pos++] = record; lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo); // dump the fifo, this starts a fresh round of data in continue_reading + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT); // dump the fifo, this starts a fresh round of data in continue_reading } static void continue_reading(accelerometer_data_acquisition_state_t *state) { printf("Continue reading\n"); lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo); + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT); fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it. uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second. diff --git a/movement.c b/movement.c index ca0b0f36..f0fb9609 100644 --- a/movement.c +++ b/movement.c @@ -2,6 +2,7 @@ * MIT License * * Copyright (c) 2022 Joey Castillo + * Copyright (c) 2025 Alessandro Genova * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,13 +24,13 @@ */ #define MOVEMENT_LONG_PRESS_TICKS 64 +#define MOVEMENT_REALLY_LONG_PRESS_TICKS 192 +#define MOVEMENT_MAX_LONG_PRESS_TICKS 1280 // get a chance to check if a button held down over 10 seconds is a glitch #include #include #include -#include #include -#include #include "app.h" #include "watch.h" #include "watch_utility.h" @@ -61,7 +62,68 @@ 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}; const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800}; -movement_event_t event; + +const uint32_t _movement_mode_button_events_mask = 0b11111 << EVENT_MODE_BUTTON_DOWN; +const uint32_t _movement_light_button_events_mask = 0b11111 << EVENT_LIGHT_BUTTON_DOWN; +const uint32_t _movement_alarm_button_events_mask = 0b11111 << EVENT_ALARM_BUTTON_DOWN; +const uint32_t _movement_button_events_mask = _movement_mode_button_events_mask | _movement_light_button_events_mask | _movement_alarm_button_events_mask; + +typedef struct { + movement_event_type_t down_event; + watch_cb_t cb_longpress; + movement_timeout_index_t timeout_index; + volatile bool is_down; + volatile rtc_counter_t down_timestamp; +#if MOVEMENT_DEBOUNCE_TICKS + volatile rtc_counter_t up_timestamp; +#endif +} movement_button_t; + +/* Pieces of state that can be modified by the various interrupt callbacks. + The interrupt writes state changes here, and it will be acted upon on the next app_loop invokation. +*/ +typedef struct { + volatile uint32_t pending_events; + volatile bool turn_led_off; + volatile bool has_pending_sequence; + volatile bool enter_sleep_mode; + volatile bool exit_sleep_mode; + volatile bool is_sleeping; + volatile uint8_t subsecond; + volatile rtc_counter_t minute_counter; + volatile bool minute_alarm_fired; + volatile bool is_buzzing; + volatile uint8_t pending_sequence_priority; + volatile bool schedule_next_comp; + volatile bool has_pending_accelerometer; + + // button tracking for long press + movement_button_t mode_button; + movement_button_t light_button; + movement_button_t alarm_button; + + // button events that will not be passed to the current face loop, but will instead passed directly to the default loop handler. + volatile uint32_t passthrough_events; +} movement_volatile_state_t; + +movement_volatile_state_t movement_volatile_state; + +// The last sequence that we have been asked to play while the watch was in deep sleep +static int8_t *_pending_sequence; + +// The note sequence of the default alarm +int8_t alarm_tune[] = { + BUZZER_NOTE_C8, 3, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 3, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 3, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 5, + BUZZER_NOTE_REST, 38, + -8, 9, + 0 +}; int8_t _movement_dst_offset_cache[NUM_ZONE_NAMES] = {0}; #define TIMEZONE_DOES_NOT_OBSERVE (-127) @@ -70,9 +132,16 @@ void cb_mode_btn_interrupt(void); void cb_light_btn_interrupt(void); void cb_alarm_btn_interrupt(void); void cb_alarm_btn_extwake(void); -void cb_alarm_fired(void); -void cb_fast_tick(void); +void cb_minute_alarm_fired(void); void cb_tick(void); +void cb_mode_btn_timeout_interrupt(void); +void cb_light_btn_timeout_interrupt(void); +void cb_alarm_btn_timeout_interrupt(void); +void cb_led_timeout_interrupt(void); +void cb_resign_timeout_interrupt(void); +void cb_sleep_timeout_interrupt(void); +void cb_buzzer_start(void); +void cb_buzzer_stop(void); void cb_accelerometer_event(void); void cb_accelerometer_wake(void); @@ -99,6 +168,54 @@ static udatetime_t _movement_convert_date_time_to_udate(watch_date_time_t date_t }; } +static watch_buzzer_volume_t _movement_get_buzzer_volume(movement_buzzer_priority_t priority) { + switch (priority) { + case BUZZER_PRIORITY_BUTTON: + return movement_button_volume(); + case BUZZER_PRIORITY_SIGNAL: + return movement_signal_volume(); + case BUZZER_PRIORITY_ALARM: + return movement_alarm_volume(); + default: + return WATCH_BUZZER_VOLUME_LOUD; + } +} + +static void _movement_set_top_of_minute_alarm() { + uint32_t counter = watch_rtc_get_counter(); + uint32_t next_minute_counter; + watch_date_time_t date_time = watch_rtc_get_date_time(); + uint32_t freq = watch_rtc_get_frequency(); + uint32_t half_freq = freq >> 1; + uint32_t subsecond_mask = freq - 1; + uint32_t ticks_per_minute = watch_rtc_get_ticks_per_minute(); + + // get the counter at the last second tick + next_minute_counter = counter & (~subsecond_mask); + // add/subtract half second shift to sync up second tick with the 1Hz interrupt + next_minute_counter += (counter & subsecond_mask) >= half_freq ? half_freq : -half_freq; + // counter at the next top of the minute + next_minute_counter += (60 - date_time.unit.second) * freq; + + // Since the minute alarm is very important, double/triple check to make sure that it will fire. + // These are theoretical corner cases that probably can't even happen, but since we do a subtraction + // above I wanna be certain that we don't schedule the next alarm at a counter value just before the + // current counter, which would result in the alarm firing after more than one year. + // This should be robust to the counter overflow, and we should ever iterate once at most. + if (next_minute_counter == counter) { + next_minute_counter += ticks_per_minute; + } + + while ((next_minute_counter - counter) > ticks_per_minute) { + next_minute_counter += ticks_per_minute; + } + + movement_volatile_state.minute_counter = next_minute_counter; + + watch_rtc_register_comp_callback_no_schedule(cb_minute_alarm_fired, next_minute_counter, MINUTE_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; +} + static bool _movement_update_dst_offset_cache(void) { uzone_t local_zone; udatetime_t udate_time; @@ -129,24 +246,122 @@ static bool _movement_update_dst_offset_cache(void) { } static inline void _movement_reset_inactivity_countdown(void) { - movement_state.le_mode_ticks = movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval]; - movement_state.timeout_ticks = movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval]; + rtc_counter_t counter = watch_rtc_get_counter(); + uint32_t freq = watch_rtc_get_frequency(); + + watch_rtc_register_comp_callback_no_schedule( + cb_resign_timeout_interrupt, + counter + movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval] * freq, + RESIGN_TIMEOUT + ); + + movement_volatile_state.enter_sleep_mode = false; + + watch_rtc_register_comp_callback_no_schedule( + cb_sleep_timeout_interrupt, + counter + movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval] * freq, + SLEEP_TIMEOUT + ); + + movement_volatile_state.schedule_next_comp = true; } -static inline void _movement_enable_fast_tick_if_needed(void) { - if (!movement_state.fast_tick_enabled) { - movement_state.fast_ticks = 0; - watch_rtc_register_periodic_callback(cb_fast_tick, 128); - movement_state.fast_tick_enabled = true; +static inline void _movement_disable_inactivity_countdown(void) { + watch_rtc_disable_comp_callback_no_schedule(RESIGN_TIMEOUT); + watch_rtc_disable_comp_callback_no_schedule(SLEEP_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; +} + +static void _movement_renew_top_of_minute_alarm(void) { + // Renew the alarm for a minute from the previous one (ensures no drift) + movement_volatile_state.minute_counter += watch_rtc_get_ticks_per_minute(); + watch_rtc_register_comp_callback_no_schedule(cb_minute_alarm_fired, movement_volatile_state.minute_counter, MINUTE_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; +} + +static uint32_t _movement_get_accelerometer_events() { + uint32_t accelerometer_events = 0; + + uint8_t int_src = lis2dw_get_interrupt_source(); + + if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { + accelerometer_events |= 1 << EVENT_DOUBLE_TAP; + printf("Double tap!\r\n"); } + + if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) { + accelerometer_events |= 1 << EVENT_SINGLE_TAP; + printf("Single tap!\r\n"); + } + + return accelerometer_events; } -static inline void _movement_disable_fast_tick_if_possible(void) { - if ((movement_state.light_ticks == -1) && - (movement_state.alarm_ticks == -1) && - ((movement_state.light_down_timestamp + movement_state.mode_down_timestamp + movement_state.alarm_down_timestamp) == 0)) { - movement_state.fast_tick_enabled = false; - watch_rtc_disable_periodic_callback(128); +static void _movement_handle_button_presses(uint32_t pending_events) { + bool any_up = false; + bool any_down = false; + bool any_long = false; + + movement_button_t* buttons[3] = { + &movement_volatile_state.mode_button, + &movement_volatile_state.light_button, + &movement_volatile_state.alarm_button + }; + + uint32_t button_events_masks[3] = { + _movement_mode_button_events_mask, + _movement_light_button_events_mask, + _movement_alarm_button_events_mask, + }; + + for (uint8_t i = 0; i < 3; i++) { + movement_button_t* button = buttons[i]; + + // If a button down occurred + if (pending_events & (1 << button->down_event)) { + watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); + any_down = true; + // this button's events will start getting passed to the face + movement_volatile_state.passthrough_events &= ~button_events_masks[i]; + } + + // If a long press occurred + if (pending_events & (1 << (button->down_event + 2))) { + watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_REALLY_LONG_PRESS_TICKS, button->timeout_index); + any_long = true; + } + + // If a really long press occurred + if (pending_events & (1 << (button->down_event + 4))) { + watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_MAX_LONG_PRESS_TICKS, button->timeout_index); + any_long = true; + } + + // If a button up or button long up occurred + if (pending_events & ( + (1 << (button->down_event + 1)) | + (1 << (button->down_event + 3)) + // (1 << (button->down_event + 5)) + )) { + // We cancel the timeout if it hasn't fired yet + watch_rtc_disable_comp_callback_no_schedule(button->timeout_index); + any_up = true; + } + } + + if (any_down) { + // force alarm off if the user pressed a button. + watch_buzzer_abort_sequence(); + + // Delay auto light off if the user is still interacting with the watch. + if (movement_state.light_on) { + movement_illuminate_led(); + } + } + + if (any_down || any_up || any_long) { + _movement_reset_inactivity_countdown(); + movement_volatile_state.schedule_next_comp = true; } } @@ -174,7 +389,6 @@ static void _movement_handle_top_of_minute(void) { // TODO: handle other advisory types } } - movement_state.woke_from_alarm_handler = false; } static void _movement_handle_scheduled_tasks(void) { @@ -205,45 +419,62 @@ static void _movement_handle_scheduled_tasks(void) { } void movement_request_tick_frequency(uint8_t freq) { - // Movement uses the 128 Hz tick internally - if (freq == 128) return; - // Movement requires at least a 1 Hz tick. // If we are asked for an invalid frequency, default back to 1 Hz. if (freq == 0 || __builtin_popcount(freq) != 1) freq = 1; - // disable all callbacks except the 128 Hz one - watch_rtc_disable_matching_periodic_callbacks(0xFE); + // disable all periodic callbacks + watch_rtc_disable_matching_periodic_callbacks(0xFF); + + // this left-justifies the period in a 32-bit integer. + uint32_t tmp = (freq & 0xFF) << 24; + // now we can count the leading zeroes to get the value we need. + // 0x01 (1 Hz) will have 7 leading zeros for PER7. 0x80 (128 Hz) will have no leading zeroes for PER0. + uint8_t per_n = __builtin_clz(tmp); - movement_state.subsecond = 0; movement_state.tick_frequency = freq; + movement_state.tick_pern = per_n; + watch_rtc_register_periodic_callback(cb_tick, freq); } void movement_illuminate_led(void) { if (movement_state.settings.bit.led_duration != 0b111) { + movement_state.light_on = true; watch_set_led_color_rgb(movement_state.settings.bit.led_red_color | movement_state.settings.bit.led_red_color << 4, movement_state.settings.bit.led_green_color | movement_state.settings.bit.led_green_color << 4, movement_state.settings.bit.led_blue_color | movement_state.settings.bit.led_blue_color << 4); if (movement_state.settings.bit.led_duration == 0) { - movement_state.light_ticks = 1; + // Do nothing it'll be turned off on button release } else { - movement_state.light_ticks = (movement_state.settings.bit.led_duration * 2 - 1) * 128; + // Set a timeout to turn off the light + rtc_counter_t counter = watch_rtc_get_counter(); + uint32_t freq = watch_rtc_get_frequency(); + watch_rtc_register_comp_callback_no_schedule( + cb_led_timeout_interrupt, + counter + (movement_state.settings.bit.led_duration * 2 - 1) * freq, + LED_TIMEOUT + ); + movement_volatile_state.schedule_next_comp = true; } - _movement_enable_fast_tick_if_needed(); } } void movement_force_led_on(uint8_t red, uint8_t green, uint8_t blue) { // this is hacky, we need a way for watch faces to set an arbitrary color and prevent Movement from turning it right back off. + movement_state.light_on = true; watch_set_led_color_rgb(red, green, blue); - movement_state.light_ticks = 32767; + // The led will stay on until movement_force_led_off is called, so disable the led timeout in case we were in the middle of it. + watch_rtc_disable_comp_callback_no_schedule(LED_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; } void movement_force_led_off(void) { + movement_state.light_on = false; + // The led timeout probably already triggered, but still disable just in case we are switching off the light by other means + watch_rtc_disable_comp_callback_no_schedule(LED_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; watch_set_led_off(); - movement_state.light_ticks = -1; - _movement_disable_fast_tick_if_possible(); } bool movement_default_loop_handler(movement_event_t event) { @@ -255,6 +486,7 @@ bool movement_default_loop_handler(movement_event_t event) { movement_illuminate_led(); break; case EVENT_LIGHT_BUTTON_UP: + case EVENT_LIGHT_LONG_UP: if (movement_state.settings.bit.led_duration == 0) { movement_force_led_off(); } @@ -317,63 +549,98 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index) { } void movement_request_sleep(void) { - /// FIXME: for #SecondMovement: This was a feature request to allow watch faces to request sleep. - /// Setting the ticks to 1 means the watch will sleep after the next tick. I'd like to say let's - /// set it to 0, have the watch face loop return false, and then we'll fall asleep immediately. - /// But could this lead to a race condition where the callback decrements to -1 before the loop? - /// This is the safest way but consider more testing here. - movement_state.le_mode_ticks = 1; + movement_volatile_state.enter_sleep_mode = true; } void movement_request_wake() { - movement_state.needs_wake = true; + movement_volatile_state.exit_sleep_mode = true; _movement_reset_inactivity_countdown(); } -static void end_buzzing() { - movement_state.is_buzzing = false; +void cb_buzzer_start(void) { + movement_volatile_state.is_buzzing = true; } -static void end_buzzing_and_disable_buzzer(void) { - end_buzzing(); - watch_disable_buzzer(); +void cb_buzzer_stop(void) { + movement_volatile_state.is_buzzing = false; + movement_volatile_state.pending_sequence_priority = 0; +} + +void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { + static int8_t single_note_sequence[3]; + + single_note_sequence[0] = note; + // 64 ticks per second for the tc0 + // Each tick is approximately 15ms + uint16_t duration = duration_ms / 15; + if (duration > 127) duration = 127; + single_note_sequence[1] = (int8_t)duration; + single_note_sequence[2] = 0; + + movement_play_sequence(single_note_sequence, BUZZER_PRIORITY_BUTTON); } void movement_play_signal(void) { - void *maybe_disable_buzzer = end_buzzing_and_disable_buzzer; - if (watch_is_buzzer_or_led_enabled()) { - maybe_disable_buzzer = end_buzzing; - } else { - watch_enable_buzzer(); - } - movement_state.is_buzzing = true; - watch_buzzer_play_sequence(signal_tune, maybe_disable_buzzer); - if (movement_state.le_mode_ticks == -1) { - // the watch is asleep. wake it up for "1" round through the main loop. - // the sleep_mode_app_loop will notice the is_buzzing and note that it - // only woke up to beep and then it will spinlock until the callback - // turns off the is_buzzing flag. - movement_state.needs_wake = true; - movement_state.le_mode_ticks = 1; - } + movement_play_sequence(signal_tune, BUZZER_PRIORITY_SIGNAL); } void movement_play_alarm(void) { - movement_play_alarm_beeps(5, BUZZER_NOTE_C8); + movement_play_sequence(alarm_tune, BUZZER_PRIORITY_ALARM); } void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note) { + // Ugly but necessary to avoid breaking backward compatibility with some faces. + // Create an alarm tune on the fly with the specified note and repetition. + static int8_t custom_alarm_tune[19]; + if (rounds == 0) rounds = 1; if (rounds > 20) rounds = 20; - movement_request_wake(); - movement_state.alarm_note = alarm_note; - // our tone is 0.375 seconds of beep and 0.625 of silence, repeated as given. - movement_state.alarm_ticks = 128 * rounds - 75; - _movement_enable_fast_tick_if_needed(); + + for (uint8_t i = 0; i < 9; i++) { + uint8_t note_idx = i * 2; + uint8_t duration_idx = note_idx + 1; + + int8_t note = alarm_tune[note_idx]; + int8_t duration = alarm_tune[duration_idx]; + + if (note == BUZZER_NOTE_C8) { + note = alarm_note; + } else if (note < 0) { + duration = rounds; + } + + custom_alarm_tune[note_idx] = note; + custom_alarm_tune[duration_idx] = duration; + } + + custom_alarm_tune[18] = 0; + + movement_play_sequence(custom_alarm_tune, BUZZER_PRIORITY_ALARM); +} + +void movement_play_sequence(int8_t *note_sequence, movement_buzzer_priority_t priority) { + // Priority is used to ensure that lower priority sequences don't cancel higher priority ones + // Priotity order: alarm(2) > signal(1) > note(0) + if (priority < movement_volatile_state.pending_sequence_priority) { + return; + } + + movement_volatile_state.pending_sequence_priority = priority; + + // The tcc is off during sleep, we can't play immediately. + // Ask to wake up the watch. + if (movement_volatile_state.is_sleeping) { + _pending_sequence = note_sequence; + movement_volatile_state.has_pending_sequence = true; + movement_volatile_state.exit_sleep_mode = true; + } else { + watch_buzzer_play_sequence_with_volume(note_sequence, NULL, _movement_get_buzzer_volume(priority)); + } } uint8_t movement_claim_backup_register(void) { - if (movement_state.next_available_backup_register >= 8) return 0; + // We use backup register 7 in watch_rtc to keep track of the reference time + if (movement_state.next_available_backup_register >= 7) return 0; return movement_state.next_available_backup_register++; } @@ -407,18 +674,44 @@ watch_date_time_t movement_get_utc_date_time(void) { watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index) { int32_t offset = movement_get_current_timezone_offset_for_zone(zone_index); - return watch_utility_date_time_convert_zone(watch_rtc_get_date_time(), 0, offset); + unix_timestamp_t timestamp = watch_rtc_get_unix_time(); + return watch_utility_date_time_from_unix_time(timestamp, offset); } watch_date_time_t movement_get_local_date_time(void) { - watch_date_time_t date_time = watch_rtc_get_date_time(); - return watch_utility_date_time_convert_zone(date_time, 0, movement_get_current_timezone_offset()); + static struct { + unix_timestamp_t timestamp; + rtc_date_time_t datetime; + } cached_date_time = {.datetime.reg=0, .timestamp=0}; + + unix_timestamp_t timestamp = watch_rtc_get_unix_time(); + + if (timestamp != cached_date_time.timestamp) { + cached_date_time.timestamp = timestamp; + cached_date_time.datetime = watch_utility_date_time_from_unix_time(timestamp, movement_get_current_timezone_offset()); + } + + return cached_date_time.datetime; +} + +uint32_t movement_get_utc_timestamp(void) { + return watch_rtc_get_unix_time(); +} + +void movement_set_utc_date_time(watch_date_time_t date_time) { + movement_set_utc_timestamp(watch_utility_date_time_to_unix_time(date_time, 0)); } void movement_set_local_date_time(watch_date_time_t date_time) { int32_t current_offset = movement_get_current_timezone_offset(); - watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0); - watch_rtc_set_date_time(utc_date_time); + movement_set_utc_timestamp(watch_utility_date_time_to_unix_time(date_time, current_offset)); +} + +void movement_set_utc_timestamp(uint32_t timestamp) { + watch_rtc_set_unix_time(timestamp); + + // If the time was changed, the top of the minute alarm needs to be reset accordingly + _movement_set_top_of_minute_alarm(); // this may seem wasteful, but if the user's local time is in a zone that observes DST, // they may have just crossed a DST boundary, which means the next call to this function @@ -426,6 +719,7 @@ void movement_set_local_date_time(watch_date_time_t date_time) { _movement_update_dst_offset_cache(); } + bool movement_button_should_sound(void) { return movement_state.settings.bit.button_should_sound; } @@ -442,6 +736,21 @@ void movement_set_button_volume(watch_buzzer_volume_t value) { movement_state.settings.bit.button_volume = value; } +watch_buzzer_volume_t movement_signal_volume(void) { + return movement_state.signal_volume; +} +void movement_set_signal_volume(watch_buzzer_volume_t value) { + movement_state.signal_volume = value; +} + +watch_buzzer_volume_t movement_alarm_volume(void) { + return movement_state.alarm_volume; +} + +void movement_set_alarm_volume(watch_buzzer_volume_t value) { + movement_state.alarm_volume = value; +} + movement_clock_mode_t movement_clock_mode_24h(void) { return movement_state.settings.bit.clock_mode_24h ? MOVEMENT_CLOCK_MODE_24H : MOVEMENT_CLOCK_MODE_12H; } @@ -516,18 +825,19 @@ bool movement_enable_tap_detection_if_available(void) { if (movement_state.has_lis2dw) { // configure tap duration threshold and enable Z axis lis2dw_configure_tap_threshold(0, 0, 12, LIS2DW_REG_TAP_THS_Z_Z_AXIS_ENABLE); - lis2dw_configure_tap_duration(10, 2, 2); + lis2dw_configure_tap_duration(2, 2, 2); // ramp data rate up to 400 Hz and high performance mode lis2dw_set_low_noise_mode(true); lis2dw_set_data_rate(LIS2DW_DATA_RATE_HP_400_HZ); - lis2dw_set_mode(LIS2DW_MODE_HIGH_PERFORMANCE); + lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); + lis2dw_enable_double_tap(); // Settling time (1 sample duration, i.e. 1/400Hz) delay_ms(3); // enable tap detection on INT1/A3. - lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_6D); + lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_DOUBLE_TAP); return true; } @@ -541,6 +851,7 @@ bool movement_disable_tap_detection_if_available(void) { lis2dw_set_low_noise_mode(false); lis2dw_set_data_rate(movement_state.accelerometer_background_rate); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); + lis2dw_disable_double_tap(); // ...disable Z axis (not sure if this is needed, does this save power?)... lis2dw_configure_tap_threshold(0, 0, 0, 0); @@ -588,6 +899,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(); @@ -598,6 +914,7 @@ float movement_get_temperature(void) { val = val >> 4; temperature_c = 25 + (float)val / 16.0; } +#endif return temperature_c; } @@ -619,6 +936,39 @@ void app_init(void) { memset((void *)&movement_state, 0, sizeof(movement_state)); + movement_volatile_state.pending_events = 0; + movement_volatile_state.turn_led_off = false; + + movement_volatile_state.minute_alarm_fired = false; + movement_volatile_state.minute_counter = 0; + + movement_volatile_state.enter_sleep_mode = false; + movement_volatile_state.exit_sleep_mode = false; + movement_volatile_state.has_pending_sequence = false; + movement_volatile_state.has_pending_accelerometer = false; + movement_volatile_state.is_sleeping = false; + + movement_volatile_state.is_buzzing = false; + movement_volatile_state.pending_sequence_priority = 0; + + movement_volatile_state.mode_button.down_event = EVENT_MODE_BUTTON_DOWN; + movement_volatile_state.mode_button.is_down = false; + movement_volatile_state.mode_button.down_timestamp = 0; + movement_volatile_state.mode_button.timeout_index = MODE_BUTTON_TIMEOUT; + movement_volatile_state.mode_button.cb_longpress = cb_mode_btn_timeout_interrupt; + + movement_volatile_state.light_button.down_event = EVENT_LIGHT_BUTTON_DOWN; + movement_volatile_state.light_button.is_down = false; + movement_volatile_state.light_button.down_timestamp = 0; + movement_volatile_state.light_button.timeout_index = LIGHT_BUTTON_TIMEOUT; + movement_volatile_state.light_button.cb_longpress = cb_light_btn_timeout_interrupt; + + movement_volatile_state.alarm_button.down_event = EVENT_ALARM_BUTTON_DOWN; + movement_volatile_state.alarm_button.is_down = false; + movement_volatile_state.alarm_button.down_timestamp = 0; + movement_volatile_state.alarm_button.timeout_index = ALARM_BUTTON_TIMEOUT; + movement_volatile_state.alarm_button.cb_longpress = cb_alarm_btn_timeout_interrupt; + movement_state.has_thermistor = thermistor_driver_init(); bool settings_file_exists = filesystem_file_exists("settings.u32"); @@ -674,15 +1024,23 @@ void app_init(void) { watch_rtc_set_date_time(date_time); } + // register callbacks to be notified when buzzer starts/stops playing. + // this is so movement can be notified even when triggered by a face bypassing movement + watch_buzzer_register_global_callbacks(cb_buzzer_start, cb_buzzer_stop); + // populate the DST offset cache _movement_update_dst_offset_cache(); if (movement_state.accelerometer_motion_threshold == 0) movement_state.accelerometer_motion_threshold = 32; - movement_state.light_ticks = -1; - movement_state.alarm_ticks = -1; + movement_state.signal_volume = MOVEMENT_DEFAULT_SIGNAL_VOLUME; + movement_state.alarm_volume = MOVEMENT_DEFAULT_ALARM_VOLUME; + movement_state.light_on = false; movement_state.next_available_backup_register = 2; _movement_reset_inactivity_countdown(); + + // set up the 1 minute alarm (for background tasks and low power updates) + _movement_set_top_of_minute_alarm(); } void app_wake_from_backup(void) { @@ -715,17 +1073,12 @@ void app_setup(void) { } } #endif - - // set up the 1 minute alarm (for background tasks and low power updates) - watch_date_time_t alarm_time; - alarm_time.reg = 0; - watch_rtc_register_alarm_callback(cb_alarm_fired, alarm_time, ALARM_MATCH_SS); } // LCD autodetect uses the buttons as a a failsafe, so we should run it before we enable the button interrupts watch_enable_display(); - if (movement_state.le_mode_ticks != -1) { + if (!movement_volatile_state.is_sleeping) { watch_disable_extwake_interrupt(HAL_GPIO_BTN_ALARM_pin()); watch_enable_external_interrupts(); @@ -792,9 +1145,6 @@ void app_setup(void) { } #endif - watch_enable_buzzer(); - watch_enable_leds(); - movement_request_tick_frequency(1); for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) { @@ -802,148 +1152,213 @@ void app_setup(void) { } watch_faces[movement_state.current_face_idx].activate(watch_face_contexts[movement_state.current_face_idx]); - event.subsecond = 0; - event.event_type = EVENT_ACTIVATE; + movement_volatile_state.pending_events |= 1 << EVENT_ACTIVATE; } } #ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN static void _sleep_mode_app_loop(void) { - movement_state.needs_wake = false; - // as long as le_mode_ticks is -1 (i.e. we are in low energy mode), we wake up here, update the screen, and go right back to sleep. - while (movement_state.le_mode_ticks == -1) { - // we also have to handle top-of-the-minute tasks here in the mini-runloop - if (movement_state.woke_from_alarm_handler) _movement_handle_top_of_minute(); + // as long as we are in low energy mode, we wake up here, update the screen, and go right back to sleep. + while (movement_volatile_state.is_sleeping) { + // if we need to wake immediately, do it! + if (movement_volatile_state.exit_sleep_mode) { + movement_volatile_state.exit_sleep_mode = false; + movement_volatile_state.is_sleeping = false; + return; + } + + // we also have to handle top-of-the-minute tasks here in the mini-runloop + if (movement_volatile_state.minute_alarm_fired) { + movement_volatile_state.minute_alarm_fired = false; + _movement_renew_top_of_minute_alarm(); + _movement_handle_top_of_minute(); + } + + movement_event_t event; event.event_type = EVENT_LOW_ENERGY_UPDATE; + event.subsecond = 0; watch_faces[movement_state.current_face_idx].loop(event, watch_face_contexts[movement_state.current_face_idx]); - // if we need to wake immediately, do it! - if (movement_state.needs_wake) return; - // otherwise enter sleep mode, and when the extwake handler is called, it will reset le_mode_ticks and force us out at the next loop. - else watch_enter_sleep_mode(); + // If any of the previous loops requested to wake up, do it! + if (movement_volatile_state.exit_sleep_mode) { + movement_volatile_state.exit_sleep_mode = false; + movement_volatile_state.is_sleeping = false; + + return; + } + + // If we have made changes to any of the RTC comp timers, schedule the next one in the queue + if (movement_volatile_state.schedule_next_comp) { + movement_volatile_state.schedule_next_comp = false; + watch_rtc_schedule_next_comp(); + } + + // otherwise enter sleep mode, until either the top of the minute interrupt or extwake wakes us up. + watch_enter_sleep_mode(); } } #endif +static bool _switch_face(void) { + const watch_face_t *wf = &watch_faces[movement_state.current_face_idx]; + + wf->resign(watch_face_contexts[movement_state.current_face_idx]); + movement_state.current_face_idx = movement_state.next_face_idx; + // we have just updated the face idx, so we must recache the watch face pointer. + wf = &watch_faces[movement_state.current_face_idx]; + watch_clear_display(); + movement_request_tick_frequency(1); + + if (movement_state.settings.bit.button_should_sound) { + // low note for nonzero case, high note for return to watch_face 0 + movement_play_note(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50); + } + + wf->activate(watch_face_contexts[movement_state.current_face_idx]); + + movement_event_t event; + event.subsecond = 0; + event.event_type = EVENT_ACTIVATE; + movement_state.watch_face_changed = false; + bool can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]); + + // Button events that follow a down event that happened on the previous face should not be forwarded to the new face + movement_volatile_state.passthrough_events = _movement_button_events_mask; + + return can_sleep; +} + bool app_loop(void) { const watch_face_t *wf = &watch_faces[movement_state.current_face_idx]; - bool woke_up_for_buzzer = false; - - if (movement_state.watch_face_changed) { - if (movement_state.settings.bit.button_should_sound) { - // low note for nonzero case, high note for return to watch_face 0 - watch_buzzer_play_note_with_volume(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50, movement_state.settings.bit.button_volume); - } - wf->resign(watch_face_contexts[movement_state.current_face_idx]); - movement_state.current_face_idx = movement_state.next_face_idx; - // we have just updated the face idx, so we must recache the watch face pointer. - wf = &watch_faces[movement_state.current_face_idx]; - watch_clear_display(); - movement_request_tick_frequency(1); - wf->activate(watch_face_contexts[movement_state.current_face_idx]); - event.subsecond = 0; - event.event_type = EVENT_ACTIVATE; - movement_state.watch_face_changed = false; - } - - // if the LED should be off, turn it off - if (movement_state.light_ticks == 0) { - // unless the user is holding down the LIGHT button, in which case, give them more time. - if (HAL_GPIO_BTN_LIGHT_read()) { - movement_state.light_ticks = 1; - } else { - movement_force_led_off(); - } - } - - // handle top-of-minute tasks, if the alarm handler told us we need to - if (movement_state.woke_from_alarm_handler) _movement_handle_top_of_minute(); - - // if we have a scheduled background task, handle that here: - if (event.event_type == EVENT_TICK && movement_state.has_scheduled_background_task) _movement_handle_scheduled_tasks(); - -#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN - // if we have timed out of our low energy mode countdown, enter low energy mode. - if (movement_state.le_mode_ticks == 0) { - movement_state.le_mode_ticks = -1; - watch_register_extwake_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_extwake, true); - event.event_type = EVENT_NONE; - event.subsecond = 0; - - // _sleep_mode_app_loop takes over at this point and loops until le_mode_ticks is reset by the extwake handler, - // or wake is requested using the movement_request_wake function. - _sleep_mode_app_loop(); - // as soon as _sleep_mode_app_loop returns, we prepare to reactivate - // ourselves, but first, we check to see if we woke up for the buzzer: - if (movement_state.is_buzzing) { - woke_up_for_buzzer = true; - } - event.event_type = EVENT_ACTIVATE; - // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks. - // need to figure out if there's a better heuristic for determining how we woke up. - app_setup(); - } -#endif // default to being allowed to sleep by the face. bool can_sleep = true; - if (event.event_type) { - event.subsecond = movement_state.subsecond; - // the first trip through the loop overrides the can_sleep state - can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]); + // Any events that have been added by the various interrupts in between app_loop invokations + uint32_t pending_events = movement_volatile_state.pending_events; + movement_volatile_state.pending_events = 0; - // Keep light on if user is still interacting with the watch. - if (movement_state.light_ticks > 0) { - switch (event.event_type) { - case EVENT_LIGHT_BUTTON_DOWN: - case EVENT_MODE_BUTTON_DOWN: - case EVENT_ALARM_BUTTON_DOWN: - movement_illuminate_led(); - } + movement_event_t event; + event.event_type = EVENT_NONE; + // Subsecond is determined by the TICK event, if concurrent events have happened, + // they will all have the same subsecond as they should to keep backward compatibility. + event.subsecond = movement_volatile_state.subsecond; + + // if the LED should be off, turn it off + if (movement_volatile_state.turn_led_off) { + // unless the user is holding down the LIGHT button, in which case, give them more time. + if (movement_volatile_state.light_button.is_down) { + } else { + movement_volatile_state.turn_led_off = false; + movement_force_led_off(); } - - event.event_type = EVENT_NONE; } - // if we have timed out of our timeout countdown, give the app a hint that they can resign. - if (movement_state.timeout_ticks == 0 && movement_state.current_face_idx != 0) { - movement_state.timeout_ticks = -1; + if (movement_volatile_state.has_pending_accelerometer) { + movement_volatile_state.has_pending_accelerometer = false; + pending_events |= _movement_get_accelerometer_events(); + } + + // handle any button up/down events that occurred, e.g. schedule longpress timeouts, reset inactivity, etc. + _movement_handle_button_presses(pending_events); + + // if we have a scheduled background task, handle that here: + if ( + (pending_events & (1 << EVENT_TICK)) + && event.subsecond == 0 + && movement_state.has_scheduled_background_task + ) { + _movement_handle_scheduled_tasks(); + } + + // Pop the EVENT_TIMEOUT out of the pending_events so it can be handled separately + bool resign_timeout = (pending_events & (1 << EVENT_TIMEOUT)) != 0; + if (resign_timeout) { + pending_events &= ~(1 << EVENT_TIMEOUT); + } + + // Consume all the pending events + uint32_t passthrough_pending_events = pending_events & movement_volatile_state.passthrough_events; + pending_events = pending_events & ~movement_volatile_state.passthrough_events; + + movement_event_type_t event_type = 0; + while (passthrough_pending_events) { + uint8_t next_event = __builtin_ctz(passthrough_pending_events); + event.event_type = event_type + next_event; + can_sleep = movement_default_loop_handler(event) && can_sleep; + passthrough_pending_events = passthrough_pending_events >> (next_event + 1); + event_type = event_type + next_event + 1; + } + + event_type = 0; + while (pending_events) { + uint8_t next_event = __builtin_ctz(pending_events); + event.event_type = event_type + next_event; + can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep; + pending_events = pending_events >> (next_event + 1); + event_type = event_type + next_event + 1; + } + + // handle top-of-minute tasks, if the alarm handler told us we need to + if (movement_volatile_state.minute_alarm_fired) { + movement_volatile_state.minute_alarm_fired = false; + _movement_renew_top_of_minute_alarm(); + _movement_handle_top_of_minute(); + } + + // Now handle the EVENT_TIMEOUT + if (resign_timeout && movement_state.current_face_idx != 0) { event.event_type = EVENT_TIMEOUT; - event.subsecond = movement_state.subsecond; - // if we run through the loop again to time out, we need to reconsider whether or not we can sleep. - // if the first trip said true, but this trip said false, we need the false to override, thus - // we will be using boolean AND: - // - // first trip | can sleep | cannot sleep | can sleep | cannot sleep - // second trip | can sleep | cannot sleep | cannot sleep | can sleep - // && | can sleep | cannot sleep | cannot sleep | cannot sleep - bool can_sleep2 = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]); - can_sleep = can_sleep && can_sleep2; - event.event_type = EVENT_NONE; + can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep; } - // Now that we've handled all display update tasks, handle the alarm. - if (movement_state.alarm_ticks >= 0) { - uint8_t buzzer_phase = (movement_state.alarm_ticks + 80) % 128; - if(buzzer_phase == 127) { - // failsafe: buzzer could have been disabled in the meantime - if (!watch_is_buzzer_or_led_enabled()) watch_enable_buzzer(); - // play 4 beeps plus pause - for(uint8_t i = 0; i < 4; i++) { - // TODO: This method of playing the buzzer blocks the UI while it's beeping. - // It might be better to time it with the fast tick. - watch_buzzer_play_note(movement_state.alarm_note, (i != 3) ? 50 : 75); - if (i != 3) watch_buzzer_play_note(BUZZER_NOTE_REST, 50); - } - } - if (movement_state.alarm_ticks == 0) { - movement_state.alarm_ticks = -1; - _movement_disable_fast_tick_if_possible(); + // The watch_face_changed flag might be set again by the face loop, so check it again + if (movement_state.watch_face_changed) { + can_sleep = _switch_face() && can_sleep; + } + +#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN + // if we have timed out of our low energy mode countdown, enter low energy mode. + if (movement_volatile_state.enter_sleep_mode && !movement_volatile_state.is_buzzing) { + movement_volatile_state.enter_sleep_mode = false; + movement_volatile_state.is_sleeping = true; + + // No need to fire resign and sleep interrupts while in sleep mode + _movement_disable_inactivity_countdown(); + + watch_register_extwake_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_extwake, true); + + // _sleep_mode_app_loop takes over at this point and loops until exit_sleep_mode is set by the extwake handler, + // or wake is requested using the movement_request_wake function. + _sleep_mode_app_loop(); + // as soon as _sleep_mode_app_loop returns, we prepare to reactivate + + // // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks. + // // need to figure out if there's a better heuristic for determining how we woke up. + app_setup(); + + // If we woke up to play a note sequence, actually play the note sequence we were asked to play while in deep sleep. + if (movement_volatile_state.has_pending_sequence) { + movement_volatile_state.has_pending_sequence = false; + watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, _movement_get_buzzer_volume(movement_volatile_state.pending_sequence_priority)); + // When this sequence is done playing, movement_request_sleep is invoked and the watch will go, + // back to sleep (unless the user interacts with it in the meantime) + _pending_sequence = NULL; } + + // don't let the watch sleep when exiting deep sleep mode, + // so that app_loop will run again and process the events that may have fired. + can_sleep = false; + } +#endif + + // If we have made changes to any of the RTC comp timers, schedule the next one in the queue + if (movement_volatile_state.schedule_next_comp) { + movement_volatile_state.schedule_next_comp = false; + watch_rtc_schedule_next_comp(); } #if __EMSCRIPTEN__ @@ -955,19 +1370,6 @@ bool app_loop(void) { } #endif - event.subsecond = 0; - - // if the watch face changed, we can't sleep because we need to update the display. - if (movement_state.watch_face_changed) can_sleep = false; - - // if we woke up for the buzzer, stay awake until it's finished. - if (woke_up_for_buzzer) { - while(watch_is_buzzer_or_led_enabled()); - } - - // if the LED is on, we need to stay awake to keep the TCC running. - if (movement_state.light_ticks != -1) can_sleep = false; - // if we are plugged into USB, we can't sleep because we need to keep the serial shell running. if (usb_is_enabled()) { yield(); @@ -977,114 +1379,161 @@ 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, volatile uint16_t *down_timestamp) { - // force alarm off if the user pressed a button. - if (movement_state.alarm_ticks) movement_state.alarm_ticks = 0; +static movement_event_type_t _process_button_event(bool pin_level, movement_button_t* button) { + movement_event_type_t event_type = EVENT_NONE; + + // This shouldn't happen normally + if (pin_level == button->is_down) { + return event_type; + } + + uint32_t counter = watch_rtc_get_counter(); + +#if MOVEMENT_DEBOUNCE_TICKS + if ( + (counter - button->up_timestamp) <= MOVEMENT_DEBOUNCE_TICKS && + (counter - button->down_timestamp) <= MOVEMENT_DEBOUNCE_TICKS + ) { + return event_type; + } +#endif + + button->is_down = pin_level; if (pin_level) { - // handle rising edge - _movement_enable_fast_tick_if_needed(); - *down_timestamp = movement_state.fast_ticks + 1; - return button_down_event_type; + button->down_timestamp = counter; + event_type = button->down_event; } else { - // this line is hack but it handles the situation where the light button was held for more than 20 seconds. - // fast tick is disabled by then, and the LED would get stuck on since there's no one left decrementing light_ticks. - if (movement_state.light_ticks == 1) movement_state.light_ticks = 0; - // now that that's out of the way, handle falling edge - uint16_t diff = movement_state.fast_ticks - *down_timestamp; - *down_timestamp = 0; - _movement_disable_fast_tick_if_possible(); - // any press over a half second is considered a long press. Fire the long-up event - if (diff > MOVEMENT_LONG_PRESS_TICKS) return button_down_event_type + 3; - else return button_down_event_type + 1; +#if MOVEMENT_DEBOUNCE_TICKS + button->up_timestamp = counter; +#endif + if ((counter - button->down_timestamp) >= MOVEMENT_REALLY_LONG_PRESS_TICKS) { + // event_type = button->down_event + 5; + event_type = button->down_event + 3; // TODO: swith to REALLY_LONG_UP + } else if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { + event_type = button->down_event + 3; + } else { + event_type = button->down_event + 1; + } } + + return event_type; } void cb_light_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_LIGHT_read(); - _movement_reset_inactivity_countdown(); - event.event_type = _figure_out_button_event(pin_level, EVENT_LIGHT_BUTTON_DOWN, &movement_state.light_down_timestamp); + + movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.light_button); } void cb_mode_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_MODE_read(); - _movement_reset_inactivity_countdown(); - event.event_type = _figure_out_button_event(pin_level, EVENT_MODE_BUTTON_DOWN, &movement_state.mode_down_timestamp); + + movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.mode_button); } void cb_alarm_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_ALARM_read(); - _movement_reset_inactivity_countdown(); - event.event_type = _figure_out_button_event(pin_level, EVENT_ALARM_BUTTON_DOWN, &movement_state.alarm_down_timestamp); + + movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.alarm_button); +} + +static movement_event_type_t _process_button_longpress_timeout(bool pin_level, movement_button_t* button) { + if (!button->is_down) { + return EVENT_NONE; + } + + uint32_t counter = watch_rtc_get_counter(); + bool max_long_press = (counter - button->down_timestamp) >= MOVEMENT_MAX_LONG_PRESS_TICKS; + bool really_long_press = (counter - button->down_timestamp) >= MOVEMENT_REALLY_LONG_PRESS_TICKS; + + if (pin_level) { + if (max_long_press) { + return EVENT_NONE; // no further events left to emit + } else if (really_long_press) { + return button->down_event + 4; // event_really_longpress + } else { + return button->down_event + 2; // event_longpress + } + } else { + // hypotetical corner case: if the timeout fired but the pin level is actually up, we may have missed/rejected the up event, so fire it here +#if MOVEMENT_DEBOUNCE_TICKS + // we're in a corner case, we don't know when the up actually happened. + button->up_timestamp = button->down_timestamp; +#endif + button->is_down = false; + if (max_long_press) { + // return button->down_event + 5; // event_really_long_up + return button->down_event + 3; // event_long_up TODO: use really_long_up + } else if (really_long_press) { + return button->down_event + 3; // event_long_up + } else { + return button->down_event + 1; // event_up + } + } +} + +void cb_light_btn_timeout_interrupt(void) { + bool pin_level = HAL_GPIO_BTN_LIGHT_read(); + movement_button_t* button = &movement_volatile_state.light_button; + + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(pin_level, button); +} + +void cb_mode_btn_timeout_interrupt(void) { + bool pin_level = HAL_GPIO_BTN_MODE_read(); + movement_button_t* button = &movement_volatile_state.mode_button; + + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(pin_level, button); +} + +void cb_alarm_btn_timeout_interrupt(void) { + bool pin_level = HAL_GPIO_BTN_ALARM_read(); + movement_button_t* button = &movement_volatile_state.alarm_button; + + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(pin_level, button); +} + +void cb_led_timeout_interrupt(void) { + movement_volatile_state.turn_led_off = true; +} + +void cb_resign_timeout_interrupt(void) { + movement_volatile_state.pending_events |= 1 << EVENT_TIMEOUT; +} + +void cb_sleep_timeout_interrupt(void) { + movement_request_sleep(); } void cb_alarm_btn_extwake(void) { // wake up! - _movement_reset_inactivity_countdown(); + movement_request_wake(); } -void cb_alarm_fired(void) { +void cb_minute_alarm_fired(void) { + movement_volatile_state.minute_alarm_fired = true; + #if __EMSCRIPTEN__ _wake_up_simulator(); #endif - - movement_state.woke_from_alarm_handler = true; -} - -void cb_fast_tick(void) { - movement_state.fast_ticks++; - if (movement_state.light_ticks > 0) movement_state.light_ticks--; - if (movement_state.alarm_ticks > 0) movement_state.alarm_ticks--; - // check timestamps and auto-fire the long-press events - // Notice: is it possible that two or more buttons have an identical timestamp? In this case - // only one of these buttons would receive the long press event. Don't bother for now... - if (movement_state.light_down_timestamp > 0) - if (movement_state.fast_ticks - movement_state.light_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) - event.event_type = EVENT_LIGHT_LONG_PRESS; - if (movement_state.mode_down_timestamp > 0) - if (movement_state.fast_ticks - movement_state.mode_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) - event.event_type = EVENT_MODE_LONG_PRESS; - if (movement_state.alarm_down_timestamp > 0) - if (movement_state.fast_ticks - movement_state.alarm_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) - event.event_type = EVENT_ALARM_LONG_PRESS; - // this is just a fail-safe; fast tick should be disabled as soon as the button is up, the LED times out, and/or the alarm finishes. - // but if for whatever reason it isn't, this forces the fast tick off after 20 seconds. - if (movement_state.fast_ticks >= 128 * 20) { - watch_rtc_disable_periodic_callback(128); - movement_state.fast_tick_enabled = false; - } } void cb_tick(void) { - event.event_type = EVENT_TICK; - watch_date_time_t date_time = watch_rtc_get_date_time(); - if (date_time.unit.second != movement_state.last_second) { - // TODO: can we consolidate these two ticks? - if (movement_state.le_mode_ticks > 0) movement_state.le_mode_ticks--; - if (movement_state.timeout_ticks > 0) movement_state.timeout_ticks--; - - movement_state.last_second = date_time.unit.second; - movement_state.subsecond = 0; - } else { - movement_state.subsecond++; - } + rtc_counter_t counter = watch_rtc_get_counter(); + uint32_t freq = watch_rtc_get_frequency(); + uint32_t half_freq = freq >> 1; + uint32_t subsecond_mask = freq - 1; + movement_volatile_state.pending_events |= 1 << EVENT_TICK; + movement_volatile_state.subsecond = ((counter + half_freq) & subsecond_mask) >> movement_state.tick_pern; } void cb_accelerometer_event(void) { - uint8_t int_src = lis2dw_get_interrupt_source(); - - if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { - event.event_type = EVENT_DOUBLE_TAP; - printf("Double tap!\n"); - } - if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) { - event.event_type = EVENT_SINGLE_TAP; - printf("Single tap!\n"); - } + movement_volatile_state.has_pending_accelerometer = true; } void cb_accelerometer_wake(void) { - event.event_type = EVENT_ACCELEROMETER_WAKE; + movement_volatile_state.pending_events |= 1 << EVENT_ACCELEROMETER_WAKE; // also: wake up! _movement_reset_inactivity_countdown(); } diff --git a/movement.h b/movement.h index 56f67a49..1bbd9638 100644 --- a/movement.h +++ b/movement.h @@ -2,6 +2,7 @@ * MIT License * * Copyright (c) 2022 Joey Castillo + * Copyright (c) 2025 Alessandro Genova * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -120,20 +121,43 @@ typedef enum { EVENT_LIGHT_BUTTON_UP, // The light button was pressed for less than half a second, and released. EVENT_LIGHT_LONG_PRESS, // The light button was held for over half a second, but not yet released. EVENT_LIGHT_LONG_UP, // The light button was held for over half a second, and released. + EVENT_LIGHT_REALLY_LONG_PRESS, // The light button was held for more than 1.5 second, note yet released. + // EVENT_LIGHT_REALLY_LONG_UP, // The light button was held for more than 1.5 second, and released. EVENT_MODE_BUTTON_DOWN, // The mode button has been pressed, but not yet released. EVENT_MODE_BUTTON_UP, // The mode button was pressed for less than half a second, and released. EVENT_MODE_LONG_PRESS, // The mode button was held for over half a second, but not yet released. EVENT_MODE_LONG_UP, // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event. + EVENT_MODE_REALLY_LONG_PRESS, // The mode button was held for more than 1.5 second, note yet released. + // EVENT_MODE_REALLY_LONG_UP, // The mode button was held for more than 1.5 second, and released. EVENT_ALARM_BUTTON_DOWN, // The alarm button has been pressed, but not yet released. EVENT_ALARM_BUTTON_UP, // The alarm button was pressed for less than half a second, and released. EVENT_ALARM_LONG_PRESS, // The alarm button was held for over half a second, but not yet released. EVENT_ALARM_LONG_UP, // The alarm button was held for over half a second, and released. + EVENT_ALARM_REALLY_LONG_PRESS, // The alarm button was held for more than 1.5 second, note yet released. + // EVENT_ALARM_REALLY_LONG_UP, // The alarm button was held for more than 1.5 second, and released. EVENT_ACCELEROMETER_WAKE, // The accelerometer has detected motion and woken up. EVENT_SINGLE_TAP, // Accelerometer detected a single tap. This event is not yet implemented. EVENT_DOUBLE_TAP, // Accelerometer detected a double tap. This event is not yet implemented. } movement_event_type_t; +// Each different timeout type will use a different index when invoking watch_rtc_register_comp_callback +typedef enum { + LIGHT_BUTTON_TIMEOUT = 0, // Light button longpress timeout + MODE_BUTTON_TIMEOUT, // Mode button longpress timeout + ALARM_BUTTON_TIMEOUT, // Alarm button longpress timeout + LED_TIMEOUT, // LED off timeout + RESIGN_TIMEOUT, // Resign active face timeout + SLEEP_TIMEOUT, // Low-energy begin timeout + MINUTE_TIMEOUT, // Top of the Minute timeout +} movement_timeout_index_t; + +typedef enum { + BUZZER_PRIORITY_BUTTON = 0, // Buzzer priority for button beeps (lowest priority). + BUZZER_PRIORITY_SIGNAL, // Buzzer priority for hourly chime (medium priority). + BUZZER_PRIORITY_ALARM, // Buzzer priority for hourly chime (highest priority). +} movement_buzzer_priority_t; + typedef struct { uint8_t event_type; uint8_t subsecond; @@ -249,37 +273,16 @@ typedef struct { int16_t current_face_idx; int16_t next_face_idx; bool watch_face_changed; - bool fast_tick_enabled; - int16_t fast_ticks; // LED stuff - int16_t light_ticks; - - // alarm stuff - int16_t alarm_ticks; - bool is_buzzing; - watch_buzzer_note_t alarm_note; - - // button tracking for long press - uint16_t light_down_timestamp; - uint16_t mode_down_timestamp; - uint16_t alarm_down_timestamp; + bool light_on; // background task handling - bool woke_from_alarm_handler; bool has_scheduled_background_task; - bool needs_wake; - - // low energy mode countdown - int32_t le_mode_ticks; - - // app resignation countdown (TODO: consolidate with LE countdown?) - int16_t timeout_ticks; // stuff for subsecond tracking uint8_t tick_frequency; - uint8_t last_second; - uint8_t subsecond; + uint8_t tick_pern; // backup register stuff uint8_t next_available_backup_register; @@ -296,6 +299,10 @@ typedef struct { lis2dw_data_rate_t accelerometer_background_rate; // threshold for considering the wearer is in motion uint8_t accelerometer_motion_threshold; + + // signal and alarm volumes + watch_buzzer_volume_t signal_volume; + watch_buzzer_volume_t alarm_volume; } movement_state_t; void movement_move_to_face(uint8_t watch_face_index); @@ -324,9 +331,11 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index); void movement_request_sleep(void); void movement_request_wake(void); +void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms); void movement_play_signal(void); void movement_play_alarm(void); void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note); +void movement_play_sequence(int8_t *note_sequence, movement_buzzer_priority_t priority); uint8_t movement_claim_backup_register(void); @@ -339,8 +348,11 @@ void movement_set_timezone_index(uint8_t value); watch_date_time_t movement_get_utc_date_time(void); watch_date_time_t movement_get_local_date_time(void); watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index); +uint32_t movement_get_utc_timestamp(void); +void movement_set_utc_date_time(watch_date_time_t date_time); void movement_set_local_date_time(watch_date_time_t date_time); +void movement_set_utc_timestamp(uint32_t timestamp); bool movement_button_should_sound(void); void movement_set_button_should_sound(bool value); @@ -348,6 +360,12 @@ void movement_set_button_should_sound(bool value); watch_buzzer_volume_t movement_button_volume(void); void movement_set_button_volume(watch_buzzer_volume_t value); +watch_buzzer_volume_t movement_signal_volume(void); +void movement_set_signal_volume(watch_buzzer_volume_t value); + +watch_buzzer_volume_t movement_alarm_volume(void); +void movement_set_alarm_volume(watch_buzzer_volume_t value); + movement_clock_mode_t movement_clock_mode_24h(void); void movement_set_clock_mode_24h(movement_clock_mode_t value); diff --git a/movement_config.h b/movement_config.h index 0da8da43..2f48d8fd 100644 --- a/movement_config.h +++ b/movement_config.h @@ -32,13 +32,13 @@ const watch_face_t watch_faces[] = { world_clock_face, sunrise_sunset_face, moon_phase_face, - stopwatch_face, + fast_stopwatch_face, countdown_face, alarm_face, temperature_display_face, voltage_face, settings_face, - set_time_face + set_time_face, }; #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) @@ -49,7 +49,7 @@ const watch_face_t watch_faces[] = { * Some folks also like to use this to hide the preferences and time set faces from the normal rotation. * If you don't want any faces to be excluded, set this to 0 and a long Mode press will have no effect. */ -#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 4) +#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 5) /* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */ #define SIGNAL_TUNE_DEFAULT @@ -68,6 +68,8 @@ const watch_face_t watch_faces[] = { #define MOVEMENT_DEFAULT_BUTTON_SOUND true #define MOVEMENT_DEFAULT_BUTTON_VOLUME WATCH_BUZZER_VOLUME_SOFT +#define MOVEMENT_DEFAULT_SIGNAL_VOLUME WATCH_BUZZER_VOLUME_LOUD +#define MOVEMENT_DEFAULT_ALARM_VOLUME WATCH_BUZZER_VOLUME_LOUD /* Set the timeout before switching back to the main watch face * Valid values are: @@ -100,4 +102,10 @@ const watch_face_t watch_faces[] = { */ #define MOVEMENT_DEFAULT_LED_DURATION 1 +/* Optionally debounce button presses (disable by default). + * A value of 4 is a good starting point if you have issues + * with multiple button presses firing. +*/ +#define MOVEMENT_DEBOUNCE_TICKS 0 + #endif // MOVEMENT_CONFIG_H_ diff --git a/movement_faces.h b/movement_faces.h index 81ca394f..3c2f5b71 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -73,5 +73,14 @@ #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" +#include "ping_face.h" +#include "rtccount_face.h" +#include "tomato_face.h" +#include "solar_time_face.h" #include "tide_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 6c8000d7..c9e4a4f1 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -22,6 +22,7 @@ SRCS += \ ./watch-faces/demo/character_set_face.c \ ./watch-faces/demo/light_sensor_face.c \ ./watch-faces/demo/peek_memory_face.c \ + ./watch-faces/demo/rtccount_face.c \ ./watch-faces/sensor/accelerometer_status_face.c \ ./watch-faces/sensor/temperature_display_face.c \ ./watch-faces/sensor/temperature_logging_face.c \ @@ -48,5 +49,13 @@ SRCS += \ ./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 \ + ./watch-faces/complication/ping_face.c \ + ./watch-faces/complication/tomato_face.c \ + ./watch-faces/clock/solar_time_face.c \ ./watch-faces/complication/tide_face.c \ # New watch faces go above this line. diff --git a/watch-faces/clock/solar_time_face.c b/watch-faces/clock/solar_time_face.c new file mode 100644 index 00000000..54592975 --- /dev/null +++ b/watch-faces/clock/solar_time_face.c @@ -0,0 +1,236 @@ +/* + * MIT License + * + * Copyright (c) 2025 Raffael Mancini + * + * 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. + * + * Solar time formulas follow the notation from: + * https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-time + */ + +#include +#include +#include +#include "solar_time_face.h" +#include "watch.h" +#include "watch_utility.h" +#include "filesystem.h" + +#if __EMSCRIPTEN__ +#include +#endif + +#ifndef M_PI +#define M_PI 3.14159265358979323846f +#endif + +/* --------------------------------------------------------------------------- + * Solar time math (pveducation.org notation) + * --------------------------------------------------------------------------- + * + * LSTM = 15 * ΔTUTC [degrees] + * B = (360 / 365) * (d - 81) [degrees] d = day-of-year + * EoT = 9.87*sin(2B) - 7.53*cos(B) + * - 1.5*sin(B) [minutes] + * TC = 4 * (Longitude - LSTM) + EoT [minutes] + * LST = LT + TC/60 [hours] + * HRA = 15 * (LST - 12) [degrees] + * --------------------------------------------------------------------------- + */ + +static movement_location_t _load_location(void) { + movement_location_t loc = {0}; + filesystem_read_file("location.u32", (char *)&loc.reg, sizeof(loc.reg)); + return loc; +} + +/* Compute and cache EoT and TC. Call when d != state->last_calc_d. */ +static void _compute_daily(solar_time_state_t *state, uint16_t d) { + /* LSTM — movement_get_current_timezone_offset() returns seconds from UTC */ + float delta_T_UTC = (float)movement_get_current_timezone_offset() / 3600.0f; + float LSTM = 15.0f * delta_T_UTC; + + movement_location_t loc = _load_location(); + float longitude = (float)(int16_t)loc.bit.longitude / 100.0f; + + /* B in radians for sinf/cosf */ + float B = (360.0f / 365.0f) * ((float)d - 81.0f) * ((float)M_PI / 180.0f); + + state->EoT = 9.87f * sinf(2.0f * B) - 7.53f * cosf(B) - 1.5f * sinf(B); + state->TC = 4.0f * (longitude - LSTM) + state->EoT; + state->last_calc_d = d; +} + +/* Recompute if the day-of-year has rolled over. Returns current d. */ +static uint16_t _maybe_recompute(solar_time_state_t *state, watch_date_time_t dt) { + uint16_t d = watch_utility_days_since_new_year( + (uint16_t)(dt.unit.year + WATCH_RTC_REFERENCE_YEAR), + dt.unit.month, + dt.unit.day + ); + if (d != state->last_calc_d && _load_location().reg != 0) { + _compute_daily(state, d); + } + return d; +} + +/* LST as total seconds since midnight (0..86399). + * LST = LT + TC/60 => in seconds: LT_sec + TC*60 */ +static int32_t _lst_seconds(watch_date_time_t dt, float TC) { + int32_t lt = (int32_t)dt.unit.hour * 3600 + + (int32_t)dt.unit.minute * 60 + + (int32_t)dt.unit.second; + int32_t tc = (int32_t)(TC * 60.0f); + return ((lt + tc) % 86400 + 86400) % 86400; +} + +static void _update_display(solar_time_state_t *state, watch_date_time_t dt) { + char bottom[7]; + + if (_load_location().reg == 0) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SOL", "SO"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, "no Loc"); + watch_clear_colon(); + return; + } + + switch (state->mode) { + + case SOLAR_TIME_MODE_LST: { + int32_t s = _lst_seconds(dt, state->TC); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SOL", "SO"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, "Ar"); + sprintf(bottom, "%02d%02d%02d", + (int)(s / 3600), (int)((s % 3600) / 60), (int)(s % 60)); + watch_set_colon(); + break; + } + + case SOLAR_TIME_MODE_NOON: { + /* Solar noon: moment when LST = 12:00 → LT_noon = 12h - TC/60 */ + int32_t s = (int32_t)(( 12.0f - state->TC / 60.0f) * 3600.0f); + s = ((s % 86400) + 86400) % 86400; + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "NOO", "NO"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, "n "); + sprintf(bottom, "%02d%02d ", (int)(s / 3600), (int)((s % 3600) / 60)); + watch_set_colon(); + break; + } + + case SOLAR_TIME_MODE_HRA: { + /* HRA = 15 * (LST - 12); negative = morning, positive = afternoon */ + int32_t s = _lst_seconds(dt, state->TC); + int16_t hra = (int16_t)roundf(15.0f * ((float)s / 3600.0f - 12.0f)); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "HrA", "Hr"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, "n "); + sprintf(bottom, "%+4d ", (int)hra); + watch_clear_colon(); + break; + } + + default: + return; + } + + watch_display_text(WATCH_POSITION_BOTTOM, bottom); +} + +/* ---- Movement callbacks -------------------------------------------------- */ + +void solar_time_face_setup(uint8_t watch_face_index, void **context_ptr) { + (void)watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(solar_time_state_t)); + memset(*context_ptr, 0, sizeof(solar_time_state_t)); + /* last_calc_d == 0 guarantees recomputation on first tick */ + } +} + +void solar_time_face_activate(void *context) { + solar_time_state_t *state = (solar_time_state_t *)context; + +#if __EMSCRIPTEN__ + /* In the simulator the browser exposes lat/lon as JS globals. + * Write them to location.u32 if not already set. */ + int16_t browser_lat = EM_ASM_INT({ return lat; }); + int16_t browser_lon = EM_ASM_INT({ return lon; }); + if (browser_lat || browser_lon) { + movement_location_t browser_loc = {0}; + filesystem_read_file("location.u32", (char *)&browser_loc.reg, sizeof(browser_loc.reg)); + if (browser_loc.reg == 0) { + browser_loc.bit.latitude = browser_lat; + browser_loc.bit.longitude = browser_lon; + filesystem_write_file("location.u32", (char *)&browser_loc.reg, sizeof(browser_loc.reg)); + } + } +#endif + + /* Force recompute on activation: timezone or location may have changed */ + state->last_calc_d = 0; + watch_date_time_t dt = movement_get_local_date_time(); + _maybe_recompute(state, dt); +} + +bool solar_time_face_loop(movement_event_t event, void *context) { + solar_time_state_t *state = (solar_time_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + case EVENT_TICK: { + watch_date_time_t dt = movement_get_local_date_time(); + _maybe_recompute(state, dt); + _update_display(state, dt); + break; + } + + case EVENT_ALARM_BUTTON_UP: + state->mode = (solar_time_mode_t)((state->mode + 1) % SOLAR_TIME_NUM_MODES); + { + watch_date_time_t dt = movement_get_local_date_time(); + _update_display(state, dt); + } + break; + + case EVENT_LOW_ENERGY_UPDATE: { + if (!watch_sleep_animation_is_running()) watch_start_sleep_animation(1000); + watch_date_time_t dt = movement_get_local_date_time(); + _maybe_recompute(state, dt); + _update_display(state, dt); + break; + } + + case EVENT_TIMEOUT: + state->mode = SOLAR_TIME_MODE_LST; + if (_load_location().reg == 0) movement_move_to_face(0); + break; + + default: + return movement_default_loop_handler(event); + } + + return true; +} + +void solar_time_face_resign(void *context) { + solar_time_state_t *state = (solar_time_state_t *)context; + state->mode = SOLAR_TIME_MODE_LST; + watch_clear_colon(); +} diff --git a/watch-faces/clock/solar_time_face.h b/watch-faces/clock/solar_time_face.h new file mode 100644 index 00000000..48faf693 --- /dev/null +++ b/watch-faces/clock/solar_time_face.h @@ -0,0 +1,84 @@ +/* + * MIT License + * + * Copyright (c) 2025 Raffael Mancini + * + * 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 + +/* + * SOLAR TIME FACE + * + * Displays solar time information based on the user's location. + * Formulas follow the notation from: + * https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-time + * + * Variables (pveducation.org notation): + * LSTM - Local Standard Time Meridian [degrees] = 15 * ΔTUTC + * B - Seasonal angle [degrees] = (360/365) * (d - 81) + * EoT - Equation of Time [minutes] = 9.87*sin(2B) - 7.53*cos(B) - 1.5*sin(B) + * TC - Time Correction Factor [minutes] = 4*(Longitude - LSTM) + EoT + * LST - Solar Time [hours] = LT + TC/60 + * HRA - Hour Angle [degrees] = 15*(LST - 12) + * + * B, EoT and TC only depend on the day-of-year d, so they are cached + * in state and recomputed exactly once per day: the cache key is d itself + * (1-366). Zero-initialisation of state guarantees a recompute on first use. + * + * Requires the location to be set via the Sunrise/Sunset face, stored in + * "location.u32" on the filesystem. If no location is set, displays + * "SO no Loc". + * + * Display modes (cycle with the Alarm / start-stop button): + * SO HH:MM:SS — Solar Time (LST), live seconds display + * nO HH:MM — Solar Noon in local clock time + * Hr ±DDD — Hour Angle (HRA) in degrees; negative=morning, positive=afternoon + */ + +#include "movement.h" + +typedef enum { + SOLAR_TIME_MODE_LST = 0, /* Solar Time SO HH:MM:SS */ + SOLAR_TIME_MODE_NOON = 1, /* Solar Noon (local) nO HH:MM */ + SOLAR_TIME_MODE_HRA = 2, /* Hour Angle Hr ±DDD */ + SOLAR_TIME_NUM_MODES +} solar_time_mode_t; + +typedef struct { + solar_time_mode_t mode; + uint16_t last_calc_d; /* day-of-year (1-366) when EoT/TC were last computed; + 0 (zero-init) guarantees recomputation on first tick */ + float EoT; /* Equation of Time [minutes] */ + float TC; /* Time Correction Factor [minutes] */ +} solar_time_state_t; + +void solar_time_face_setup(uint8_t watch_face_index, void **context_ptr); +void solar_time_face_activate(void *context); +bool solar_time_face_loop(movement_event_t event, void *context); +void solar_time_face_resign(void *context); + +#define solar_time_face ((const watch_face_t){ \ + solar_time_face_setup, \ + solar_time_face_activate, \ + solar_time_face_loop, \ + solar_time_face_resign, \ + NULL, \ +}) diff --git a/watch-faces/complication/advanced_alarm_face.c b/watch-faces/complication/advanced_alarm_face.c index 39739bc4..4e5d9eb9 100644 --- a/watch-faces/complication/advanced_alarm_face.c +++ b/watch-faces/complication/advanced_alarm_face.c @@ -40,9 +40,9 @@ typedef enum { alarm_setting_idx_beeps } alarm_setting_idx_t; -static const char _dow_strings[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SO", "ED", "1t", "MF", "WN"}; -static const uint8_t _blink_idx[ALARM_SETTING_STATES] = {2, 0, 4, 6, 8, 9}; -static const uint8_t _blink_idx2[ALARM_SETTING_STATES] = {3, 1, 5, 7, 8, 9}; +static const char _dow_strings_classic[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SU", "ED", "1t", "MF", "WN"}; +static const char _dow_strings_custom[ALARM_DAY_STATES + 1][3] ={ "AL ", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN", "DAY", "1t ", "M-F", "WKD"}; +static const uint8_t _beeps_blink_idx = 9; static const watch_buzzer_note_t _buzzer_notes[3] = {BUZZER_NOTE_B6, BUZZER_NOTE_C8, BUZZER_NOTE_A8}; // Volume is indicated by the three segments 5D, 5G and 5A @@ -67,6 +67,10 @@ static void _alarm_set_signal(alarm_state_t *state) { watch_clear_indicator(WATCH_INDICATOR_SIGNAL); } +static void _alarm_show_alarm_on_text(alarm_state_t *state) { + watch_display_text(WATCH_POSITION_SECONDS, state->alarm[state->alarm_idx].enabled ? "on" : "--"); +} + static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) { char buf[12]; bool set_leading_zero = movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_024H; @@ -90,18 +94,34 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) { watch_set_indicator(WATCH_INDICATOR_24H); } - sprintf(buf, set_leading_zero? "%c%c%2d%02d%02d " : "%c%c%2d%2d%02d ", - _dow_strings[i][0], _dow_strings[i][1], - (state->alarm_idx + 1), - h, - state->alarm[state->alarm_idx].minute); // blink items if in settings mode - if (state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks) { - buf[_blink_idx[state->setting_state]] = buf[_blink_idx2[state->setting_state]] = ' '; + bool blinking = state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks; + if (state->setting_state == alarm_setting_idx_alarm && blinking) { + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + } else { + sprintf(buf, "%2d", (state->alarm_idx + 1)); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); + } + if (state->setting_state == alarm_setting_idx_day && blinking) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, " ", " "); + } else { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, _dow_strings_custom[i], _dow_strings_classic[i]); + } + if (state->setting_state == alarm_setting_idx_hour && blinking) { + watch_display_text(WATCH_POSITION_HOURS, " "); + } else { + sprintf(buf, set_leading_zero? "%02d" : "%2d", h); + watch_display_text(WATCH_POSITION_HOURS, buf); + } + if (state->setting_state == alarm_setting_idx_minute && blinking) { + watch_display_text(WATCH_POSITION_MINUTES, " "); + } else { + sprintf(buf, "%02d", state->alarm[state->alarm_idx].minute); + watch_display_text(WATCH_POSITION_MINUTES, buf); } - watch_display_text(WATCH_POSITION_FULL, buf); if (state->is_setting) { + watch_display_text(WATCH_POSITION_SECONDS, " "); // draw pitch level indicator if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_pitch)) { for (i = 0; i <= state->alarm[state->alarm_idx].pitch && i < 3; i++) @@ -110,15 +130,18 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) { // draw beep rounds indicator if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_beeps)) { if (state->alarm[state->alarm_idx].beeps == ALARM_MAX_BEEP_ROUNDS - 1) - watch_display_character('L', _blink_idx[alarm_setting_idx_beeps]); + watch_display_character('L', _beeps_blink_idx); else { if (state->alarm[state->alarm_idx].beeps == 0) - watch_display_character('o', _blink_idx[alarm_setting_idx_beeps]); + watch_display_character('o', _beeps_blink_idx); else - watch_display_character(state->alarm[state->alarm_idx].beeps + 48, _blink_idx[alarm_setting_idx_beeps]); + watch_display_character(state->alarm[state->alarm_idx].beeps + 48, _beeps_blink_idx); } } } + else { + _alarm_show_alarm_on_text(state); + } // set alarm indicator _alarm_set_signal(state); @@ -179,9 +202,16 @@ static void _alarm_update_alarm_enabled(alarm_state_t *state) { static void _alarm_play_short_beep(uint8_t pitch_idx) { // play a short double beep - watch_buzzer_play_note(_buzzer_notes[pitch_idx], 50); - watch_buzzer_play_note(BUZZER_NOTE_REST, 50); - watch_buzzer_play_note(_buzzer_notes[pitch_idx], 70); + static int8_t beep_sequence[] = { + 0, 4, + BUZZER_NOTE_REST, 4, + 0, 6, + 0 + }; + beep_sequence[0] = _buzzer_notes[pitch_idx]; + beep_sequence[4] = _buzzer_notes[pitch_idx]; + + movement_play_sequence(beep_sequence, 0); } static void _alarm_indicate_beep(alarm_state_t *state) { @@ -303,6 +333,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) { // revert change of enabled flag and show it briefly state->alarm[state->alarm_idx].enabled ^= 1; _alarm_set_signal(state); + _alarm_show_alarm_on_text(state); delay_ms(275); state->alarm_idx = 0; } @@ -413,14 +444,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) { // play alarm if (state->alarm[state->alarm_playing_idx].beeps == 0) { // short beep - if (watch_is_buzzer_or_led_enabled()) { - _alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch); - } else { - // enable, play beep and disable buzzer again - watch_enable_buzzer(); - _alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch); - watch_disable_buzzer(); - } + _alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch); } else { // regular alarm beeps movement_play_alarm_beeps((state->alarm[state->alarm_playing_idx].beeps == (ALARM_MAX_BEEP_ROUNDS - 1) ? 20 : state->alarm[state->alarm_playing_idx].beeps), diff --git a/watch-faces/complication/baby_kicks_face.c b/watch-faces/complication/baby_kicks_face.c index bfbc0756..cc0dd513 100644 --- a/watch-faces/complication/baby_kicks_face.c +++ b/watch-faces/complication/baby_kicks_face.c @@ -210,7 +210,7 @@ static inline void _display_counts(baby_kicks_state_t *state) { watch_display_text(WATCH_POSITION_BOTTOM, "baby "); watch_clear_colon(); } else { - char buf[7]; + char buf[9]; snprintf( buf, @@ -259,7 +259,7 @@ static void _display_elapsed_minutes(baby_kicks_state_t *state) { * the total elapsed minutes. */ - char buf[3]; + char buf[5]; uint32_t elapsed_minutes = _elapsed_minutes(state); uint8_t multiple = elapsed_minutes / 30; uint8_t remainder = elapsed_minutes % 30; diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c new file mode 100755 index 00000000..c5dda6b6 --- /dev/null +++ b/watch-faces/complication/blackjack_face.c @@ -0,0 +1,472 @@ +/* + * MIT License + * + * Copyright (c) 2025 David Volovskiy + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// Emulator only: need time() to seed the random number generator. +#if __EMSCRIPTEN__ +#include +#endif + +#include +#include +#include "blackjack_face.h" +#include "watch_common_display.h" + +#define ACE 14 +#define KING 13 +#define QUEEN 12 +#define JACK 11 + +#define MIN_CARD_VALUE 2 +#define MAX_CARD_VALUE ACE +#define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1) +#define CARD_SUIT_COUNT 4 +#define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT) + +#define BLACKJACK_MAX_HAND_SIZE 11 // 4*1 + 4*2 + 3*3 = 21; 11 cards total +#define MAX_PLAYER_CARDS_DISPLAY 4 +#define BOARD_DISPLAY_START 4 + +typedef struct { + uint8_t score; + uint8_t idx_hand; + int8_t high_aces_in_hand; + uint8_t hand[BLACKJACK_MAX_HAND_SIZE]; +} hand_info_t; + +typedef enum { + BJ_TITLE_SCREEN, + BJ_PLAYING, + BJ_DEALER_PLAYING, + BJ_BUST, + BJ_RESULT, + BJ_WIN_RATIO, +} game_state_t; + +typedef enum { + A, B, C, D, E, F, G +} segment_t; + +static bool tap_turned_on = false; +static game_state_t game_state; +static uint8_t deck[DECK_SIZE] = {0}; +static uint8_t current_card = 0; +static blackjack_face_state_t *g_state = NULL; +hand_info_t player; +hand_info_t dealer; + +static uint8_t generate_random_number(uint8_t num_values) { + // Emulator: use rand. Hardware: use arc4random. +#if __EMSCRIPTEN__ + return rand() % num_values; +#else + return arc4random_uniform(num_values); +#endif +} + +static void stack_deck(void) { + for (size_t i = 0; i < CARD_RANK_COUNT; i++) { + for (size_t j = 0; j < CARD_SUIT_COUNT; j++) + deck[(i * CARD_SUIT_COUNT) + j] = MIN_CARD_VALUE + i; + } +} + +static void shuffle_deck(void) { + // Randomize shuffle with Fisher Yates + size_t i; + size_t j; + uint8_t tmp; + + for (i = DECK_SIZE - 1; i > 0; i--) { + j = generate_random_number(0xFF) % (i + 1); + tmp = deck[j]; + deck[j] = deck[i]; + deck[i] = tmp; + } +} + +static void reset_deck(void) { + current_card = 0; + shuffle_deck(); +} + +static uint8_t get_next_card(void) { + if (current_card >= DECK_SIZE) + reset_deck(); + return deck[current_card++]; +} + +static uint8_t get_card_value(uint8_t card) { + switch (card) + { + case ACE: + return 11; + case KING: + case QUEEN: + case JACK: + return 10; + default: + return card; + } +} + +static void modify_score_from_aces(hand_info_t *hand_info) { + while (hand_info->score > 21 && hand_info->high_aces_in_hand > 0) { + hand_info->score -= 10; + hand_info->high_aces_in_hand--; + } +} + +static void reset_hands(void) { + memset(&player, 0, sizeof(player)); + memset(&dealer, 0, sizeof(dealer)); + reset_deck(); +} + +static void give_card(hand_info_t *hand_info) { + uint8_t card = get_next_card(); + if (card == ACE) hand_info->high_aces_in_hand++; + hand_info->hand[hand_info->idx_hand++] = card; + uint8_t card_value = get_card_value(card); + hand_info->score += card_value; + modify_score_from_aces(hand_info); +} + +static void set_segment_at_position(segment_t segment, uint8_t position) { + digit_mapping_t segmap; + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) { + segmap = Custom_LCD_Display_Mapping[position]; + } else { + segmap = Classic_LCD_Display_Mapping[position]; + } + const uint8_t com_pin = segmap.segment[segment].address.com; + const uint8_t seg = segmap.segment[segment].address.seg; + watch_set_pixel(com_pin, seg); +} + +static void display_card_at_position(uint8_t card, uint8_t display_position) { + switch (card) { + case KING: + watch_display_character(' ', display_position); + set_segment_at_position(A, display_position); + set_segment_at_position(D, display_position); + set_segment_at_position(G, display_position); + break; + case QUEEN: + watch_display_character(' ', display_position); + set_segment_at_position(A, display_position); + set_segment_at_position(D, display_position); + break; + case JACK: + watch_display_character('-', display_position); + break; + case ACE: + watch_display_character(watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM ? 'A' : 'a', display_position); + break; + case 10: + watch_display_character('0', display_position); + break; + default: { + const char display_char = card + '0'; + watch_display_character(display_char, display_position); + break; + } + } +} + +static void display_player_hand(void) { + uint8_t card; + if (player.idx_hand <= MAX_PLAYER_CARDS_DISPLAY) { + card = player.hand[player.idx_hand - 1]; + display_card_at_position(card, BOARD_DISPLAY_START + player.idx_hand - 1); + } else { + for (uint8_t i=0; igames_played++; + if (g_state->games_played == 0) { + // Overflow + g_state->games_played = 1; + g_state->games_won = add_to_wins ? 1 : 0; + return; + } + if (add_to_wins) { + g_state->games_won++; + if (g_state->games_won == 0) { + // Overflow + g_state->games_played = 1; + g_state->games_won = 1; + } + } +} + +static void display_win(void) { + game_state = BJ_RESULT; + add_to_game_scores(true); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WlN ", " WIN"); + display_score(player.score, WATCH_POSITION_SECONDS); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); +} + +static void display_lose(void) { + game_state = BJ_RESULT; + add_to_game_scores(false); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "LOSE", "lOSE"); + display_score(player.score, WATCH_POSITION_SECONDS); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); +} + +static void display_tie(void) { + game_state = BJ_RESULT; + // Don't record ties to the win ratio + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "TlE ", " TIE"); + display_score(player.score, WATCH_POSITION_SECONDS); +} + +static void display_bust(void) { + game_state = BJ_RESULT; + add_to_game_scores(false); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST", "BUST"); +} + +static void display_title(void) { + game_state = BJ_TITLE_SCREEN; + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "BLACK ", "21"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " JACK ", "BLaKJK"); +} + +static void display_win_ratio(blackjack_face_state_t *state) { + char buf[7]; + game_state = BJ_WIN_RATIO; + uint8_t win_ratio = 0; + if (state->games_played > 0) { // Avoid dividing by zero + win_ratio = (uint8_t)((100 * state->games_won) / state->games_played); + } + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "WINS ", "WR"); + sprintf(buf, "%3dPct", win_ratio); + watch_display_text(WATCH_POSITION_BOTTOM, buf); +} + +static void begin_playing(bool tap_control_on) { + watch_clear_display(); + if (tap_control_on) { + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } + game_state = BJ_PLAYING; + reset_hands(); + // Give player their first 2 cards + give_card(&player); + display_player_hand(); + give_card(&player); + display_player_hand(); + display_score(player.score, WATCH_POSITION_SECONDS); + give_card(&dealer); + display_dealer_hand(); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); +} + +static void perform_stand(void) { + game_state = BJ_DEALER_PLAYING; + watch_display_text(WATCH_POSITION_BOTTOM, "Stnd"); + display_score(player.score, WATCH_POSITION_SECONDS); +} + +static void perform_hit(void) { + if (player.score == 21) { + perform_stand(); + return; // Assume hitting on 21 is a mistake and stand + } + give_card(&player); + if (player.score > 21) { + game_state = BJ_BUST; + } + display_player_hand(); + display_score(player.score, WATCH_POSITION_SECONDS); +} + +static void dealer_performs_hits(void) { + give_card(&dealer); + display_dealer_hand(); + if (dealer.score > 21) { + display_win(); + } else if (dealer.score > player.score) { + display_lose(); + } else { + display_dealer_hand(); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); + } +} + +static void see_if_dealer_hits(void) { + if (dealer.score > 16) { + if (dealer.score > player.score) { + display_lose(); + } else if (dealer.score < player.score) { + display_win(); + } else { + display_tie(); + } + } else { + dealer_performs_hits(); + } +} + +static void handle_button_presses(bool tap_control_on, bool hit) { + switch (game_state) + { + case BJ_TITLE_SCREEN: + if (!tap_turned_on && tap_control_on) { + if (movement_enable_tap_detection_if_available()) tap_turned_on = true; + } + begin_playing(tap_control_on); + break; + case BJ_PLAYING: + if (hit) { + perform_hit(); + } else { + perform_stand(); + } + break; + case BJ_DEALER_PLAYING: + see_if_dealer_hits(); + break; + case BJ_BUST: + display_bust(); + break; + case BJ_RESULT: + case BJ_WIN_RATIO: + display_title(); + break; + } +} + +static void toggle_tap_control(blackjack_face_state_t *state) { + if (state->tap_control_on) { + movement_disable_tap_detection_if_available(); + state->tap_control_on = false; + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + } else { + bool tap_could_enable = movement_enable_tap_detection_if_available(); + if (tap_could_enable) { + state->tap_control_on = true; + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } + } +} + +void blackjack_face_setup(uint8_t watch_face_index, void **context_ptr) { + (void) watch_face_index; + + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(blackjack_face_state_t)); + memset(*context_ptr, 0, sizeof(blackjack_face_state_t)); + blackjack_face_state_t *state = (blackjack_face_state_t *)*context_ptr; + state->tap_control_on = false; + } + g_state = (blackjack_face_state_t *)*context_ptr; +} + +void blackjack_face_activate(void *context) { + blackjack_face_state_t *state = (blackjack_face_state_t *) context; + (void) state; + display_title(); + stack_deck(); +} + +bool blackjack_face_loop(movement_event_t event, void *context) { + blackjack_face_state_t *state = (blackjack_face_state_t *) context; + switch (event.event_type) { + case EVENT_ACTIVATE: + if (state->tap_control_on) watch_set_indicator(WATCH_INDICATOR_SIGNAL); + break; + case EVENT_TICK: + if (game_state == BJ_DEALER_PLAYING) { + see_if_dealer_hits(); + } + else if (game_state == BJ_BUST) { + display_bust(); + } + break; + case EVENT_LIGHT_BUTTON_UP: + case EVENT_DOUBLE_TAP: + handle_button_presses(state->tap_control_on, false); + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_ALARM_BUTTON_UP: + case EVENT_SINGLE_TAP: + handle_button_presses(state->tap_control_on, true); + break; + case EVENT_LIGHT_LONG_PRESS: + if (game_state == BJ_TITLE_SCREEN) { + display_win_ratio(state); + } else { + movement_illuminate_led(); + } + break; + case EVENT_ALARM_LONG_PRESS: + if (game_state == BJ_TITLE_SCREEN) { + toggle_tap_control(state); + } else if (game_state == BJ_WIN_RATIO) { + // Reset the win-lose ratio + state->games_won = 0; + state->games_played = 0; + watch_display_text(WATCH_POSITION_BOTTOM, " 0Pct"); + } + break; + case EVENT_TIMEOUT: + case EVENT_LOW_ENERGY_UPDATE: + if (tap_turned_on) { + movement_disable_tap_detection_if_available(); + } + break; + default: + return movement_default_loop_handler(event); + } + return true; +} + +void blackjack_face_resign(void *context) { + (void) context; + if (tap_turned_on) { + tap_turned_on = false; + movement_disable_tap_detection_if_available(); + } +} diff --git a/watch-faces/complication/blackjack_face.h b/watch-faces/complication/blackjack_face.h new file mode 100755 index 00000000..8b10d069 --- /dev/null +++ b/watch-faces/complication/blackjack_face.h @@ -0,0 +1,96 @@ +/* + * 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 will stand if you try to 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 on the Title Screen 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. Warning: if you're using the LIS2DW board, it cannot register a double-tapping + * without seeing a single-tap first. + * + * Long-pressing the LIGHT button on the Title Screen will display your win rate as a percentage of games finished. + * It displays as games won / (games won + games lost) it does not include incomplete nor tied games. + * You can reset the win rate on that screen by long-pressing the ALARM button. + * + * | Cards | | + * |---------|--------------------------| + * | Value |2|3|4|5|6|7|8|9|10|J|Q|K|A| + * | Display |2|3|4|5|6|7|8|9| 0|-|=|≡|a| + * If you're using a custom display, Ace will display as 'A', not 'a' + */ + + + +typedef struct { + bool tap_control_on; + uint16_t games_played; + uint16_t games_won; +} blackjack_face_state_t; + +void blackjack_face_setup(uint8_t watch_face_index, void ** context_ptr); +void blackjack_face_activate(void *context); +bool blackjack_face_loop(movement_event_t event, void *context); +void blackjack_face_resign(void *context); + +#define blackjack_face ((const watch_face_t){ \ + blackjack_face_setup, \ + blackjack_face_activate, \ + blackjack_face_loop, \ + blackjack_face_resign, \ + NULL, \ +}) + +#endif // blackjack_FACE_H_ diff --git a/watch-faces/complication/deadline_face.c b/watch-faces/complication/deadline_face.c index 41240e76..ebad0e8b 100644 --- a/watch-faces/complication/deadline_face.c +++ b/watch-faces/complication/deadline_face.c @@ -23,80 +23,6 @@ * THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/* - * # Deadline Face - * - * This is a watch face for tracking deadlines. It draws inspiration from - * other watch faces of the project but focuses on keeping track of - * deadlines. You can enter and monitor up to four different deadlines by - * providing their respective date and time. The face has two modes: - * *running mode* and *settings mode*. - * - * ## Running Mode - * - * When the watch face is activated, it defaults to running mode. The top - * right corner shows the current deadline number, and the main display - * presents the time left until the deadline. The format of the display - * varies depending on the remaining time. - * - * - When less than a day is left, the display shows the remaining hours, - * minutes, and seconds in the form `HH:MM:SS`. - * - * - When less than a month is left, the display shows the remaining days - * and hours in the form `DD:HH` with the unit `dy` for days. - * - * - When less than a year is left, the display shows the remaining months - * and days in the form `MM:DD` with the unit `mo` for months. - * - * - When more than a year is left, the years and months are displayed in - * the form `YY:MM` with the unit `yr` for years. - * - * - When a deadline has passed in the last 24 hours, the display shows - * `over` to indicate that the deadline has just recently been reached. - * - * - When no deadline is set for a particular slot, or if a deadline has - * already passed by more than 24 hours, `--:--` is displayed. - * - * The user can navigate in running mode using the following buttons: - * - * - The *alarm button* moves the next deadline. There are currently four - * slots available for deadlines. When the last slot has been reached, - * pressing the button moves to the first slot. - * - * - A *long press* on the *alarm button* activates settings mode and - * enables configuring the currently selected deadline. - * - * - A *long press* on the *light button* activates a deadline alarm. The - * bell icon is displayed, and the alarm will ring upon reaching any of - * the deadlines set. It is important to note that the watch will not - * enter low-energy sleep mode while the alarm is enabled. - * - * - * ## Settings Mode - * - * In settings mode, the currently selected slot for a deadline can be - * configured by providing the date and the time. Like running mode, the - * top right corner of the display indicates the current deadline number. - * The main display shows the date and, on the next page, the time to be - * configured. - * - * The user can use the following buttons in settings mode. - * - * - The *light button* navigates through the different date and time - * settings, going from year, month, day, hour, to minute. The selected - * position is blinking. - * - * - A *long press* on the light button resets the date and time to the next - * day at midnight. This is the default deadline. - * - * - The *alarm button* increments the currently selected position. A *long - * press* on the *alarm button* changes the value faster. - * - * - The *mode button* exists setting mode and returns to *running mode*. - * Here the selected deadline slot can be changed. - * - */ - #include #include #include "deadline_face.h" @@ -156,26 +82,36 @@ static inline int _days_in_month(int16_t month, int16_t year) /* Play beep sound based on type */ static inline void _beep(beep_type_t beep_type) { + static int8_t beep_sequence[] = { + 0, 4, + 0, 6, + 0, 6, + 0 + }; + if (!movement_button_should_sound()) return; switch (beep_type) { case BEEP_BUTTON: - watch_buzzer_play_note(BUZZER_NOTE_C7, 50); + beep_sequence[0] = BUZZER_NOTE_C7; + beep_sequence[2] = 0; break; case BEEP_ENABLE: - watch_buzzer_play_note(BUZZER_NOTE_G7, 50); - watch_buzzer_play_note(BUZZER_NOTE_REST, 75); - watch_buzzer_play_note(BUZZER_NOTE_C8, 75); + beep_sequence[0] = BUZZER_NOTE_G7; + beep_sequence[2] = BUZZER_NOTE_REST; + beep_sequence[4] = BUZZER_NOTE_C8; break; case BEEP_DISABLE: - watch_buzzer_play_note(BUZZER_NOTE_C8, 50); - watch_buzzer_play_note(BUZZER_NOTE_REST, 75); - watch_buzzer_play_note(BUZZER_NOTE_G7, 75); + beep_sequence[0] = BUZZER_NOTE_C8; + beep_sequence[2] = BUZZER_NOTE_REST; + beep_sequence[4] = BUZZER_NOTE_G7; break; } + + movement_play_sequence(beep_sequence, 0); } /* Change tick frequency */ diff --git a/watch-faces/complication/deadline_face.h b/watch-faces/complication/deadline_face.h index 98a50d2d..6da3c893 100644 --- a/watch-faces/complication/deadline_face.h +++ b/watch-faces/complication/deadline_face.h @@ -26,6 +26,80 @@ #ifndef DEADLINE_FACE_H_ #define DEADLINE_FACE_H_ +/* + * # 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 "movement.h" /* Modes of face */ diff --git a/legacy/watch_faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c similarity index 69% rename from legacy/watch_faces/complication/endless_runner_face.c rename to watch-faces/complication/endless_runner_face.c index 1a173293..8a4b7a3b 100644 --- a/legacy/watch_faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -25,6 +25,7 @@ #include #include #include "endless_runner_face.h" +#include "delay.h" typedef enum { JUMPING_FINAL_FRAME = 0, @@ -34,6 +35,7 @@ typedef enum { typedef enum { SCREEN_TITLE = 0, + SCREEN_SCORE, SCREEN_PLAYING, SCREEN_LOSE, SCREEN_TIME, @@ -77,14 +79,45 @@ typedef struct { uint8_t fuel; } game_state_t; +// always-on, left, right, bottom, jump-top, jump-left, jump-right +int8_t classic_ball_arr_com[] = {1, 0, 1, 0, 2, 1, 2}; +int8_t classic_ball_arr_seg[] = {20, 20, 21, 21, 20, 17, 21}; +int8_t custom_ball_arr_com[] = {2, 1, 1, 0, 3, 3, 2}; +int8_t custom_ball_arr_seg[] = {15, 15, 14, 15, 14, 15, 14}; + +// obstacle 0-11 +int8_t classic_obstacle_arr_com[] = {0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1}; +int8_t classic_obstacle_arr_seg[] = {18, 19, 20, 21, 22, 23, 0, 1, 2, 4, 5, 6}; +int8_t custom_obstacle_arr_com[] = {1, 1, 1, 1, 1, 0, 1, 0, 3, 0, 0, 2}; +int8_t custom_obstacle_arr_seg[] = {22, 16, 15, 14, 1, 2, 3, 4, 4, 5, 6, 7}; + +int8_t *ball_arr_com; +int8_t *ball_arr_seg; +int8_t *obstacle_arr_com; +int8_t *obstacle_arr_seg; + static game_state_t game_state; static const uint8_t _num_bits_obst_pattern = sizeof(game_state.obst_pattern) * 8; +int8_t start_tune[] = { + BUZZER_NOTE_C5, 15, + BUZZER_NOTE_E5, 15, + BUZZER_NOTE_G5, 15, + 0 +}; + +int8_t lose_tune[] = { + BUZZER_NOTE_D3, 10, + BUZZER_NOTE_C3SHARP_D3FLAT, 10, + BUZZER_NOTE_C3, 10, + 0 +}; + static void print_binary(uint32_t value, int bits) { #if __EMSCRIPTEN__ for (int i = bits - 1; i >= 0; i--) { // Print each bit - printf("%lu", (value >> i) & 1); + printf("%u", (value >> i) & 1); // Optional: add a space every 4 bits for readability if (i % 4 == 0 && i != 0) { printf(" "); @@ -188,22 +221,22 @@ static uint32_t get_random_legal(uint32_t prev_val, uint16_t difficulty) { static void display_ball(bool jumping) { if (!jumping) { - watch_set_pixel(0, 21); - watch_set_pixel(1, 21); - watch_set_pixel(0, 20); - watch_set_pixel(1, 20); - watch_clear_pixel(1, 17); - watch_clear_pixel(2, 20); - watch_clear_pixel(2, 21); + watch_set_pixel(ball_arr_com[3], ball_arr_seg[3]); + watch_set_pixel(ball_arr_com[2], ball_arr_seg[2]); + watch_set_pixel(ball_arr_com[1], ball_arr_seg[1]); + watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]); + watch_clear_pixel(ball_arr_com[6], ball_arr_seg[6]); + watch_clear_pixel(ball_arr_com[5], ball_arr_seg[5]); + watch_clear_pixel(ball_arr_com[4], ball_arr_seg[4]); } else { - watch_clear_pixel(0, 21); - watch_clear_pixel(1, 21); - watch_clear_pixel(0, 20); - watch_set_pixel(1, 20); - watch_set_pixel(1, 17); - watch_set_pixel(2, 20); - watch_set_pixel(2, 21); + watch_clear_pixel(ball_arr_com[3], ball_arr_seg[3]); + watch_clear_pixel(ball_arr_com[2], ball_arr_seg[2]); + watch_clear_pixel(ball_arr_com[1], ball_arr_seg[1]); + watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]); + watch_set_pixel(ball_arr_com[6], ball_arr_seg[6]); + watch_set_pixel(ball_arr_com[5], ball_arr_seg[5]); + watch_set_pixel(ball_arr_com[4], ball_arr_seg[4]); } } @@ -212,12 +245,12 @@ static void display_score(uint8_t score) { if (game_state.fuel_mode) { score %= (MAX_DISP_SCORE_FUEL + 1); sprintf(buf, "%1d", score); - watch_display_string(buf, 0); + watch_display_text(WATCH_POSITION_TOP_LEFT, buf); } else { score %= (MAX_DISP_SCORE + 1); sprintf(buf, "%2d", score); - watch_display_string(buf, 2); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } } @@ -234,16 +267,16 @@ static void add_to_score(endless_runner_state_t *state) { static void display_fuel(uint8_t subsecond, uint8_t difficulty) { char buf[4]; if (difficulty == DIFF_FUEL_1 && game_state.fuel == 0 && subsecond % (FREQ/2) == 0) { - watch_display_string(" ", 2); // Blink the 0 fuel to show it cannot be refilled. + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); // Blink the 0 fuel to show it cannot be refilled. return; } sprintf(buf, "%2d", game_state.fuel); - watch_display_string(buf, 2); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } static void check_and_reset_hi_score(endless_runner_state_t *state) { // Resets the hi score at the beginning of each month. - watch_date_time_t date_time = watch_rtc_get_date_time(); + watch_date_time_t date_time = movement_get_local_date_time(); if ((state -> year_last_hi_score != date_time.unit.year) || (state -> month_last_hi_score != date_time.unit.month)) { @@ -255,28 +288,15 @@ static void check_and_reset_hi_score(endless_runner_state_t *state) { } static void display_difficulty(uint16_t difficulty) { - switch (difficulty) - { - case DIFF_BABY: - watch_display_string(" b", 2); - break; - case DIFF_EASY: - watch_display_string(" E", 2); - break; - case DIFF_HARD: - watch_display_string(" H", 2); - break; - case DIFF_FUEL: - watch_display_string(" F", 2); - break; - case DIFF_FUEL_1: - watch_display_string("1F", 2); - break; - case DIFF_NORM: - default: - watch_display_string(" N", 2); - break; - } + static const char *labels[] = { + [DIFF_BABY] = " b", + [DIFF_EASY] = " E", + [DIFF_HARD] = " H", + [DIFF_FUEL] = " F", + [DIFF_FUEL_1] = "1F", + [DIFF_NORM] = " N" + }; + watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]); game_state.fuel_mode = difficulty >= DIFF_FUEL && difficulty <= DIFF_FUEL_1; } @@ -289,65 +309,93 @@ static void change_difficulty(endless_runner_state_t *state) { } } -static void toggle_sound(endless_runner_state_t *state) { - state -> soundOn = !state -> soundOn; - if (state -> soundOn){ - watch_buzzer_play_note(BUZZER_NOTE_C5, 30); +static void display_sound_indicator(bool soundOn) { + if (soundOn){ watch_set_indicator(WATCH_INDICATOR_BELL); - } - else { + } else { watch_clear_indicator(WATCH_INDICATOR_BELL); } } +static void toggle_sound(endless_runner_state_t *state) { + state -> soundOn = !state -> soundOn; + display_sound_indicator(state -> soundOn); + if (state -> soundOn){ + watch_buzzer_play_note(BUZZER_NOTE_C5, 30); + } +} + +static void enable_tap_control(endless_runner_state_t *state) { + if (!state->tap_control_on) { + movement_enable_tap_detection_if_available(); + state->tap_control_on = true; + } +} + +static void disable_tap_control(endless_runner_state_t *state) { + if (state->tap_control_on) { + movement_disable_tap_detection_if_available(); + state->tap_control_on = false; + } +} + static void display_title(endless_runner_state_t *state) { + game_state.curr_screen = SCREEN_TITLE; + watch_clear_colon(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "ENdLS", "ER "); + watch_display_text(WATCH_POSITION_BOTTOM, "RUNNER"); + display_sound_indicator(state -> soundOn); +} + +static void display_score_screen(endless_runner_state_t *state) { uint16_t hi_score = state -> hi_score; uint8_t difficulty = state -> difficulty; bool sound_on = state -> soundOn; - game_state.curr_screen = SCREEN_TITLE; memset(&game_state, 0, sizeof(game_state)); + game_state.curr_screen = SCREEN_SCORE; game_state.sec_before_moves = 1; // The first obstacles will all be 0s, which is about an extra second of delay. if (sound_on) game_state.sec_before_moves--; // Start chime is about 1 second watch_set_colon(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN ", "ER "); if (hi_score > MAX_HI_SCORE) { - watch_display_string("ER HS --", 0); + watch_display_text(WATCH_POSITION_BOTTOM, "HS --"); } else { - char buf[14]; - sprintf(buf, "ER HS%4d", hi_score); - watch_display_string(buf, 0); + char buf[10]; + sprintf(buf, "HS%4d", hi_score); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } display_difficulty(difficulty); + display_sound_indicator(sound_on); } -static void display_time(watch_date_time_t date_time, bool clock_mode_24h) { +static void display_time(void) { static watch_date_time_t previous_date_time; + watch_date_time_t date_time = movement_get_local_date_time(); + movement_clock_mode_t clock_mode_24h = movement_clock_mode_24h(); char buf[6 + 1]; // If the hour needs updating or it's the first time displaying the time if ((game_state.curr_screen != SCREEN_TIME) || (date_time.unit.hour != previous_date_time.unit.hour)) { uint8_t hour = date_time.unit.hour; game_state.curr_screen = SCREEN_TIME; - - if (clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + if (!watch_sleep_animation_is_running()) { + watch_set_colon(); + watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500); + } + if (clock_mode_24h != MOVEMENT_CLOCK_MODE_12H) watch_set_indicator(WATCH_INDICATOR_24H); else { if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM); hour %= 12; if (hour == 0) hour = 12; } - watch_set_colon(); - sprintf( buf, "%2d%02d ", hour, date_time.unit.minute); - watch_display_string(buf, 4); + sprintf( buf, clock_mode_24h == MOVEMENT_CLOCK_MODE_024H ? "%02d%02d " : "%2d%02d ", hour, date_time.unit.minute); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } - // If both digits of the minute need updating - else if ((date_time.unit.minute / 10) != (previous_date_time.unit.minute / 10)) { - sprintf( buf, "%02d ", date_time.unit.minute); - watch_display_string(buf, 6); - } - // If only the ones-place of the minute needs updating. - else if (date_time.unit.minute != previous_date_time.unit.minute) { - sprintf( buf, "%d ", date_time.unit.minute % 10); - watch_display_string(buf, 7); + // If only the minute need updating + else { + sprintf( buf, "%02d", date_time.unit.minute); + watch_display_text(WATCH_POSITION_MINUTES, buf); } previous_date_time.reg = date_time.reg; } @@ -356,36 +404,37 @@ static void begin_playing(endless_runner_state_t *state) { uint8_t difficulty = state -> difficulty; game_state.curr_screen = SCREEN_PLAYING; watch_clear_colon(); + display_sound_indicator(state -> soundOn); movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ); if (game_state.fuel_mode) { - watch_display_string(" ", 0); + watch_clear_display(); game_state.obst_pattern = get_random_fuel(0); if ((16 * JUMP_FRAMES_FUEL_RECHARGE) < JUMP_FRAMES_FUEL) // 16 frames of zeros at the start of a level game_state.fuel = JUMP_FRAMES_FUEL - (16 * JUMP_FRAMES_FUEL_RECHARGE); // Have it below its max to show it counting up when starting. if (game_state.fuel < JUMP_FRAMES_FUEL_RECHARGE) game_state.fuel = JUMP_FRAMES_FUEL_RECHARGE; } else { - watch_display_string(" ", 2); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, " "); game_state.obst_pattern = get_random_legal(0, difficulty); } game_state.jump_state = NOT_JUMPING; display_ball(game_state.jump_state != NOT_JUMPING); display_score( game_state.curr_score); if (state -> soundOn){ - watch_buzzer_play_note(BUZZER_NOTE_C5, 200); - watch_buzzer_play_note(BUZZER_NOTE_E5, 200); - watch_buzzer_play_note(BUZZER_NOTE_G5, 200); + watch_buzzer_play_sequence(start_tune, NULL); } } static void display_lose_screen(endless_runner_state_t *state) { game_state.curr_screen = SCREEN_LOSE; game_state.curr_score = 0; - watch_display_string(" LOSE ", 0); - if (state -> soundOn) - watch_buzzer_play_note(BUZZER_NOTE_A1, 600); - else + watch_clear_display(); + watch_display_text(WATCH_POSITION_BOTTOM, " LOSE "); + if (state -> soundOn) { + watch_buzzer_play_sequence(lose_tune, NULL); delay_ms(600); + } } static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t *state) { @@ -395,9 +444,9 @@ static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t case 2: game_state.loc_2_on = obstacle; if (obstacle) - watch_set_pixel(0, 20); + watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); else if (game_state.jump_state != NOT_JUMPING) { - watch_clear_pixel(0, 20); + watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); if (game_state.fuel_mode && prev_obst_pos_two) add_to_score(state); } @@ -406,55 +455,20 @@ static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t case 3: game_state.loc_3_on = obstacle; if (obstacle) - watch_set_pixel(1, 21); + watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); else if (game_state.jump_state != NOT_JUMPING) - watch_clear_pixel(1, 21); + watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); break; case 1: if (!game_state.fuel_mode && obstacle) // If an obstacle is here, it means the ball cleared it add_to_score(state); //fall through - case 0: - case 5: - if (obstacle) - watch_set_pixel(0, 18 + grid_loc); - else - watch_clear_pixel(0, 18 + grid_loc); - break; - case 4: - if (obstacle) - watch_set_pixel(1, 22); - else - watch_clear_pixel(1, 22); - break; - case 6: - if (obstacle) - watch_set_pixel(1, 0); - else - watch_clear_pixel(1, 0); - break; - case 7: - case 8: - if (obstacle) - watch_set_pixel(0, grid_loc - 6); - else - watch_clear_pixel(0, grid_loc - 6); - break; - case 9: - case 10: - if (obstacle) - watch_set_pixel(0, grid_loc - 5); - else - watch_clear_pixel(0, grid_loc - 5); - break; - case 11: - if (obstacle) - watch_set_pixel(1, 6); - else - watch_clear_pixel(1, 6); - break; default: + if (obstacle) + watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); + else + watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); break; } } @@ -546,26 +560,37 @@ void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr) { memset(*context_ptr, 0, sizeof(endless_runner_state_t)); endless_runner_state_t *state = (endless_runner_state_t *)*context_ptr; state->difficulty = DIFF_NORM; + state->tap_control_on = false; } } void endless_runner_face_activate(void *context) { (void) context; + bool is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; + ball_arr_com = is_custom_lcd ? custom_ball_arr_com : classic_ball_arr_com; + ball_arr_seg = is_custom_lcd ? custom_ball_arr_seg : classic_ball_arr_seg; + obstacle_arr_com = is_custom_lcd ? custom_obstacle_arr_com : classic_obstacle_arr_com; + obstacle_arr_seg = is_custom_lcd ? custom_obstacle_arr_seg : classic_obstacle_arr_seg; + if (watch_sleep_animation_is_running()) { + watch_stop_blink(); + } } bool endless_runner_face_loop(movement_event_t event, void *context) { endless_runner_state_t *state = (endless_runner_state_t *)context; switch (event.event_type) { case EVENT_ACTIVATE: + disable_tap_control(state); check_and_reset_hi_score(state); - if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL); display_title(state); break; case EVENT_TICK: switch (game_state.curr_screen) { case SCREEN_TITLE: + case SCREEN_SCORE: case SCREEN_LOSE: + case SCREEN_TIME: break; default: update_game(state, event.subsecond); @@ -574,15 +599,37 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { break; case EVENT_LIGHT_BUTTON_UP: case EVENT_ALARM_BUTTON_UP: - if (game_state.curr_screen == SCREEN_TITLE) - begin_playing(state); - else if (game_state.curr_screen == SCREEN_LOSE) - display_title(state); + switch (game_state.curr_screen) { + case SCREEN_SCORE: + enable_tap_control(state); + begin_playing(state); + break; + case SCREEN_TITLE: + enable_tap_control(state); + // fall through + case SCREEN_TIME: + case SCREEN_LOSE: + watch_clear_display(); + display_score_screen(state); + } break; case EVENT_LIGHT_LONG_PRESS: - if (game_state.curr_screen == SCREEN_TITLE) + if (game_state.curr_screen == SCREEN_SCORE) change_difficulty(state); break; + case EVENT_SINGLE_TAP: + case EVENT_DOUBLE_TAP: + if (state->difficulty > DIFF_HARD) break; // Don't do this on fuel modes + // Allow starting a new game by tapping. + if (game_state.curr_screen == SCREEN_SCORE) { + begin_playing(state); + break; + } + else if (game_state.curr_screen == SCREEN_LOSE) { + display_score_screen(state); + break; + } + //fall through case EVENT_LIGHT_BUTTON_DOWN: case EVENT_ALARM_BUTTON_DOWN: if (game_state.curr_screen == SCREEN_PLAYING && game_state.jump_state == NOT_JUMPING){ @@ -592,15 +639,21 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { } break; case EVENT_ALARM_LONG_PRESS: - if (game_state.curr_screen != SCREEN_PLAYING) + if (game_state.curr_screen == SCREEN_TITLE || game_state.curr_screen == SCREEN_SCORE) toggle_sound(state); break; case EVENT_TIMEOUT: - if (game_state.curr_screen != SCREEN_TITLE) - display_title(state); + disable_tap_control(state); + if (game_state.curr_screen != SCREEN_SCORE) + display_score_screen(state); break; case EVENT_LOW_ENERGY_UPDATE: - display_time(watch_rtc_get_date_time(), movement_clock_mode_24h()); + if (game_state.curr_screen != SCREEN_TIME) { + watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN ", "ER "); + display_sound_indicator(state -> soundOn); + display_difficulty(state->difficulty); + } + display_time(); break; default: return movement_default_loop_handler(event); @@ -609,6 +662,6 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { } void endless_runner_face_resign(void *context) { - (void) context; + endless_runner_state_t *state = (endless_runner_state_t *)context; + disable_tap_control(state); } - diff --git a/legacy/watch_faces/complication/endless_runner_face.h b/watch-faces/complication/endless_runner_face.h similarity index 91% rename from legacy/watch_faces/complication/endless_runner_face.h rename to watch-faces/complication/endless_runner_face.h index 3cfa6814..3a17a611 100644 --- a/legacy/watch_faces/complication/endless_runner_face.h +++ b/watch-faces/complication/endless_runner_face.h @@ -33,6 +33,8 @@ This is a basic endless-runner, like the [Chrome Dino game](https://en.wikipedia.org/wiki/Dinosaur_Game). On the title screen, you can select a difficulty by long-pressing LIGHT or toggle sound by long-pressing ALARM. LED or ALARM are used to jump. + If the accelerometer is installed, you can tap the screen to jump and move through the menus after using the + buttons to go into the first game. High-score is displayed on the top-right on the title screen. During a game, the current score is displayed. */ @@ -42,7 +44,8 @@ typedef struct { uint8_t month_last_hi_score : 4; uint8_t year_last_hi_score : 6; uint8_t soundOn : 1; - /* 24 bits, likely aligned to 32 bits = 4 bytes */ + uint8_t tap_control_on : 1; + uint8_t unused : 7; } endless_runner_state_t; void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr); diff --git a/watch-faces/complication/fast_stopwatch_face.c b/watch-faces/complication/fast_stopwatch_face.c index ce998102..70c1de96 100644 --- a/watch-faces/complication/fast_stopwatch_face.c +++ b/watch-faces/complication/fast_stopwatch_face.c @@ -2,6 +2,7 @@ * MIT License * * Copyright (c) 2022 Andreas Nebinger + * Copyright (c) 2025 Alessandro Genova * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,11 +25,13 @@ #include #include +#include #include "fast_stopwatch_face.h" #include "watch.h" #include "watch_common_display.h" #include "watch_utility.h" #include "watch_rtc.h" +#include "slcd.h" /* This watch face implements the original F-91W stopwatch functionality @@ -40,173 +43,247 @@ turns on on each button press or it doesn't. */ -#if __EMSCRIPTEN__ -#include -#include -#else -#include "tc.h" -#endif - -// distant future for background task: January 1, 2083 -static const watch_date_time_t distant_future = { - .unit = {0, 0, 0, 1, 1, 63} -}; - -static uint32_t _ticks; -static uint32_t _lap_ticks; -static uint8_t _blink_ticks; -static uint32_t _old_seconds; -static uint8_t _old_minutes; -static uint8_t _hours; -static bool _colon; -static bool _is_running; - -#if __EMSCRIPTEN__ - -static long _em_interval_id = 0; - -void em_cb_handler(void *userData) { - // interrupt handler for emscripten 128 Hz callbacks - (void) userData; - _ticks++; -} - -static void _cb_initialize() { } - -static inline void _cb_stop() { - emscripten_clear_interval(_em_interval_id); - _em_interval_id = 0; - _is_running = false; -} - -static inline void _cb_start() { - // initiate 128 hz callback - _em_interval_id = emscripten_set_interval(em_cb_handler, (double)(1000/128), (void *)NULL); -} - -#else - -static inline void _cb_start() { - // start the TC1 timer - tc_enable(1); - _is_running = true; -} - -static inline void _cb_stop() { - // stop the TC1 timer - tc_disable(1); - _is_running = false; -} - -static void _cb_initialize() { - tc_init(1, GENERIC_CLOCK_3, TC_PRESCALER_DIV4); - tc_set_counter_mode(1, TC_COUNTER_MODE_8BIT); - tc_set_run_in_standby(1, true); - _cb_stop(); - tc_count8_set_period(1, 1); // 1024 Hz divided by 4 divided by 2 results in a 128 Hz interrupt - /// FIXME: #SecondMovement, we need a gossamer wrapper for interrupts. - TC1->COUNT8.INTENSET.bit.OVF = 1; - NVIC_ClearPendingIRQ(TC1_IRQn); - NVIC_EnableIRQ (TC1_IRQn); -} - -void irq_handler_tc1(void); -void irq_handler_tc1(void) { - // interrupt handler for TC1 (globally!) - _ticks++; - TC1->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; -} - -#endif +// Loosely implement the watch as a state machine +typedef enum { + SW_STATUS_IDLE = 0, + SW_STATUS_RUNNING, + SW_STATUS_RUNNING_LAPPING, + SW_STATUS_STOPPED, + SW_STATUS_STOPPED_LAPPING +} stopwatch_status_t; static inline void _button_beep() { // play a beep as confirmation for a button press (if applicable) if (movement_button_should_sound()) watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume()); } +// How quickly should the elapsing time be displayed? +// This is just for looks, timekeeping is always accurate to 128Hz +static const uint8_t DISPLAY_RUNNING_RATE = 32; +static const uint8_t DISPLAY_RUNNING_RATE_SLOW = 2; + /// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter /// on the lcd. /// @param ticks -static void _display_ticks(uint32_t ticks) { - char buf[14]; - uint8_t sec_100 = (ticks & 0x7F) * 100 / 128; +static void _display_elapsed(fast_stopwatch_state_t *state, uint32_t ticks) { + char buf[3]; + + if (state->slow_refresh && (state->status == SW_STATUS_RUNNING || state->status == SW_STATUS_IDLE)) { + watch_display_character_lp_seconds(' ', 8); + watch_display_character_lp_seconds(' ', 9); + } else { + uint8_t sec_100 = (ticks & 0x7F) * 100 / 128; + + watch_display_character_lp_seconds('0' + sec_100 / 10, 8); + watch_display_character_lp_seconds('0' + sec_100 % 10, 9); + } + uint32_t seconds = ticks >> 7; + + if (seconds == state->old_display.seconds) { + return; + } + + state->old_display.seconds = seconds; + + sprintf(buf, "%02lu", seconds % 60); + watch_display_text(WATCH_POSITION_MINUTES, buf); + uint32_t minutes = seconds / 60; - if (_hours) { - sprintf(buf, "%2u", _hours); + + if (minutes == state->old_display.minutes) { + return; + } + + state->old_display.minutes = minutes; + + sprintf(buf, "%02lu", minutes % 60); + watch_display_text(WATCH_POSITION_HOURS, buf); + + uint32_t hours = (minutes / 60) % 24; + + if (hours == state->old_display.hours) { + return; + } + + state->old_display.hours = hours; + + if (hours) { + sprintf(buf, "%2lu", hours); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } else { watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); } - - sprintf(buf, "%02lu%02lu%02u", minutes, (seconds % 60), sec_100); - watch_display_text(WATCH_POSITION_BOTTOM, buf); } -/// @brief Displays the current stopwatch time on the LCD (more optimized than _display_ticks()) -static void _draw() { - if (_lap_ticks == 0) { - char buf[14]; - uint8_t sec_100 = (_ticks & 0x7F) * 100 / 128; - if (_is_running) { - uint32_t seconds = _ticks >> 7; - if (seconds != _old_seconds) { - // seconds have changed - _old_seconds = seconds; - uint8_t minutes = seconds / 60; - seconds %= 60; - if (minutes != _old_minutes) { - // minutes have changed, draw everything - _old_minutes = minutes; - minutes %= 60; - if (_hours) { - // with hour indicator - sprintf(buf, "%2u", _hours); - watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); - } else { - // no hour indicator - watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); - } - sprintf(buf, "%02u%02lu%02u", minutes, seconds, sec_100); - watch_display_text(WATCH_POSITION_BOTTOM, buf); - } else { - // just draw seconds - sprintf(buf, "%02lu", seconds); - // note that we're drawing the seconds in the "minutes" position, since this - // watch face uses the "seconds" position for hundredths of seconds - watch_display_text(WATCH_POSITION_MINUTES, buf); - watch_display_character_lp_seconds('0' + sec_100 / 10, 8); - watch_display_character_lp_seconds('0' + sec_100 % 10, 9); - } +static void _draw_indicators(fast_stopwatch_state_t *state, movement_event_t event, uint32_t elapsed) { + uint8_t subsecond; + bool tock; + + switch (state->status) { + case SW_STATUS_RUNNING: + subsecond = elapsed & 127; + tock = subsecond >= 64; + + watch_clear_indicator(WATCH_INDICATOR_LAP); + if (tock) { + watch_clear_colon(); } else { - // only draw 100ths of seconds - watch_display_character_lp_seconds('0' + sec_100 / 10, 8); - watch_display_character_lp_seconds('0' + sec_100 % 10, 9); + watch_set_colon(); } - } else { - _display_ticks(_ticks); - } - } - if (_is_running) { - // blink the colon every half second - uint8_t blink_ticks = ((_ticks >> 6) & 1); - if (blink_ticks != _blink_ticks) { - _blink_ticks = blink_ticks; - _colon = !_colon; - if (_colon) watch_set_colon(); - else watch_clear_colon(); - } + + return; + + case SW_STATUS_RUNNING_LAPPING: + tock = event.subsecond > 0; + + if (tock) { + watch_clear_indicator(WATCH_INDICATOR_LAP); + watch_clear_colon(); + } else { + watch_set_indicator(WATCH_INDICATOR_LAP); + watch_set_colon(); + } + + return; + + case SW_STATUS_STOPPED_LAPPING: + watch_set_indicator(WATCH_INDICATOR_LAP); + watch_set_colon(); + + return; + + case SW_STATUS_STOPPED: + case SW_STATUS_IDLE: + default: + watch_clear_indicator(WATCH_INDICATOR_LAP); + watch_set_colon(); + return; } } -static inline void _update_lap_indicator() { - if (_lap_ticks) watch_set_indicator(WATCH_INDICATOR_LAP); - else watch_clear_indicator(WATCH_INDICATOR_LAP); +static uint8_t get_refresh_rate(fast_stopwatch_state_t *state) { + switch (state->status) { + case SW_STATUS_RUNNING: + if (state->slow_refresh) { + return DISPLAY_RUNNING_RATE_SLOW; + } else { + return DISPLAY_RUNNING_RATE; + } + case SW_STATUS_RUNNING_LAPPING: + return 2; + case SW_STATUS_STOPPED: + case SW_STATUS_IDLE: + default: + return 1; + } } -static inline void _set_colon() { - watch_set_colon(); - _colon = true; +static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counter, movement_event_type_t event_type) { + switch (state->status) { + case SW_STATUS_IDLE: + switch (event_type) { + case EVENT_ALARM_BUTTON_DOWN: + state->status = SW_STATUS_RUNNING; + state->start_counter = counter; + movement_request_tick_frequency(get_refresh_rate(state)); + return; + case EVENT_LIGHT_LONG_PRESS: + state->slow_refresh = !state->slow_refresh; + return; + default: + return; + } + + case SW_STATUS_RUNNING: + switch (event_type) { + case EVENT_ALARM_BUTTON_DOWN: + state->status = SW_STATUS_STOPPED; + state->stop_counter = counter; + movement_request_tick_frequency(get_refresh_rate(state)); + return; + case EVENT_LIGHT_BUTTON_DOWN: + state->status = SW_STATUS_RUNNING_LAPPING; + state->lap_counter = counter; + movement_request_tick_frequency(get_refresh_rate(state)); + return; + default: + return; + } + + case SW_STATUS_RUNNING_LAPPING: + switch (event_type) { + case EVENT_ALARM_BUTTON_DOWN: + state->status = SW_STATUS_STOPPED_LAPPING; + state->stop_counter = counter; + movement_request_tick_frequency(get_refresh_rate(state)); + return; + case EVENT_LIGHT_BUTTON_DOWN: + state->status = SW_STATUS_RUNNING; + state->lap_counter = counter; + movement_request_tick_frequency(get_refresh_rate(state)); + return; + case EVENT_LIGHT_LONG_PRESS: + state->status = SW_STATUS_RUNNING; + state->slow_refresh = !state->slow_refresh; + movement_request_tick_frequency(get_refresh_rate(state)); + return; + default: + return; + } + + case SW_STATUS_STOPPED_LAPPING: + switch (event_type) { + case EVENT_ALARM_BUTTON_DOWN: + state->status = SW_STATUS_RUNNING_LAPPING; + state->start_counter = counter - state->stop_counter + state->start_counter; + state->lap_counter = counter - state->stop_counter + state->lap_counter; + movement_request_tick_frequency(get_refresh_rate(state)); + return; + case EVENT_LIGHT_BUTTON_DOWN: + state->status = SW_STATUS_STOPPED; + return; + default: + return; + } + + case SW_STATUS_STOPPED: + switch (event_type) { + case EVENT_ALARM_BUTTON_DOWN: + state->status = SW_STATUS_RUNNING; + state->start_counter = counter - state->stop_counter + state->start_counter; + movement_request_tick_frequency(get_refresh_rate(state)); + return; + case EVENT_LIGHT_BUTTON_DOWN: + state->status = SW_STATUS_IDLE; + return; + default: + return; + } + + default: + return; + } +} + +static uint32_t elapsed_time(fast_stopwatch_state_t *state, rtc_counter_t counter) { + switch (state->status) { + case SW_STATUS_IDLE: + return 0; + + case SW_STATUS_RUNNING: + return counter - state->start_counter; + + case SW_STATUS_RUNNING_LAPPING: + case SW_STATUS_STOPPED_LAPPING: + return state->lap_counter - state->start_counter; + + case SW_STATUS_STOPPED: + return state->stop_counter - state->start_counter; + + default: + return 0; + } } void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) { @@ -215,114 +292,54 @@ void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) { *context_ptr = malloc(sizeof(fast_stopwatch_state_t)); memset(*context_ptr, 0, sizeof(fast_stopwatch_state_t)); fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)*context_ptr; - _ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; - _is_running = _colon = false; - state->light_on_button = true; - } - if (!_is_running) { - // prepare the 128 Hz callback source - _cb_initialize(); + state->start_counter = 0; + state->stop_counter = 0; + state->lap_counter = 0; + state->status = SW_STATUS_IDLE; } } void fast_stopwatch_face_activate(void *context) { - (void) context; - if (_is_running) { - // The background task will keep the watch from entering low energy mode while the stopwatch is on screen. - movement_schedule_background_task(distant_future); - } + fast_stopwatch_state_t *state = (fast_stopwatch_state_t *) context; + // force full re-draw + state->old_display.seconds = UINT_MAX; + state->old_display.minutes = UINT_MAX; + state->old_display.hours = UINT_MAX; + movement_request_tick_frequency(get_refresh_rate(state)); } bool fast_stopwatch_face_loop(movement_event_t event, void *context) { fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)context; - // handle overflow of fast ticks - while (_ticks >= (128 * 60 * 60)) { - _ticks -= (128 * 60 * 60); - _hours++; - if (_hours >= 24) _hours -= 24; - // initiate a re-draw - _old_minutes = 59; - } + rtc_counter_t counter = watch_rtc_get_counter(); + + state_transition(state, counter, event.event_type); + rtc_counter_t elapsed = elapsed_time(state, counter); switch (event.event_type) { case EVENT_ACTIVATE: - _set_colon(); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST"); - _update_lap_indicator(); - if (_is_running) movement_request_tick_frequency(16); - _display_ticks(_lap_ticks ? _lap_ticks : _ticks); - break; - case EVENT_TICK: - _draw(); - break; - case EVENT_LIGHT_LONG_PRESS: - // kind od hidden feature: long press toggles light on or off - state->light_on_button = !state->light_on_button; - if (state->light_on_button) movement_illuminate_led(); - else watch_set_led_off(); + _draw_indicators(state, event, elapsed); + _display_elapsed(state, elapsed); break; case EVENT_ALARM_BUTTON_DOWN: - _is_running = !_is_running; - if (_is_running) { - // start or continue stopwatch - movement_request_tick_frequency(16); - // register 128 hz callback for time measuring - _cb_start(); - // schedule the keepalive task when running - movement_schedule_background_task(distant_future); - } else { - // stop the stopwatch - _cb_stop(); - movement_request_tick_frequency(1); - _set_colon(); - // cancel the keepalive task - movement_cancel_background_task(); - } - _draw(); - _button_beep(); - break; case EVENT_LIGHT_BUTTON_DOWN: - if (state->light_on_button) movement_illuminate_led(); - if (_is_running) { - if (_lap_ticks) { - // clear lap and continue running - _lap_ticks = 0; - movement_request_tick_frequency(16); - } else { - // set lap ticks and stop updating the display - _lap_ticks = _ticks; - movement_request_tick_frequency(2); - _set_colon(); - } - } else { - if (_lap_ticks) { - // clear lap and show running stopwatch - _lap_ticks = 0; - } else if (_ticks) { - // reset stopwatch - _ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; - _button_beep(); - } - } - _display_ticks(_ticks); - _update_lap_indicator(); - break; - case EVENT_TIMEOUT: - if (!_is_running) movement_move_to_face(0); - break; - case EVENT_LOW_ENERGY_UPDATE: - _draw(); + case EVENT_LIGHT_LONG_PRESS: + _button_beep(); + // fall through + case EVENT_TICK: + _draw_indicators(state, event, elapsed); + _display_elapsed(state, elapsed); break; default: movement_default_loop_handler(event); break; } + return true; } void fast_stopwatch_face_resign(void *context) { (void) context; - // cancel the keepalive task - movement_cancel_background_task(); + movement_request_tick_frequency(1); } diff --git a/watch-faces/complication/fast_stopwatch_face.h b/watch-faces/complication/fast_stopwatch_face.h index eac56651..34cb4973 100644 --- a/watch-faces/complication/fast_stopwatch_face.h +++ b/watch-faces/complication/fast_stopwatch_face.h @@ -55,7 +55,16 @@ #include "movement.h" typedef struct { - bool light_on_button; // determines whether the light button actually triggers the led + rtc_counter_t start_counter; // rtc counter when the stopwatch was started + rtc_counter_t lap_counter; // rtc counter when the stopwatch was lapped + rtc_counter_t stop_counter; // rtc counter when the stopwatch was stopped + uint8_t status; // the status the stopwatch is in (idle, running, stopped) + bool slow_refresh; // update the display slowly (same 128Hz timekeeping accuracy) + struct { + rtc_counter_t seconds; + rtc_counter_t minutes; + rtc_counter_t hours; + } old_display; // the digits currently being displayed on screen } fast_stopwatch_state_t; void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr); @@ -63,12 +72,6 @@ void fast_stopwatch_face_activate(void *context); bool fast_stopwatch_face_loop(movement_event_t event, void *context); void fast_stopwatch_face_resign(void *context); -#if __EMSCRIPTEN__ -void em_cb_handler(void *userData); -#else -void TC2_Handler(void); -#endif - #define fast_stopwatch_face ((const watch_face_t){ \ fast_stopwatch_face_setup, \ fast_stopwatch_face_activate, \ diff --git a/legacy/watch_faces/complication/higher_lower_game_face.c b/watch-faces/complication/higher_lower_game_face.c similarity index 81% rename from legacy/watch_faces/complication/higher_lower_game_face.c rename to watch-faces/complication/higher_lower_game_face.c index 3491b5bd..d47e1f61 100755 --- a/legacy/watch_faces/complication/higher_lower_game_face.c +++ b/watch-faces/complication/higher_lower_game_face.c @@ -30,19 +30,22 @@ #include #include #include "higher_lower_game_face.h" -#include "watch_private_display.h" +#include "watch_common_display.h" + + +#define KING 12 +#define QUEEN 11 +#define JACK 10 #define TITLE_TEXT "Hi-Lo" #define GAME_BOARD_SIZE 6 #define MAX_BOARDS 40 #define GUESSES_PER_SCREEN 5 #define WIN_SCORE (MAX_BOARDS * GUESSES_PER_SCREEN) -#define STATUS_DISPLAY_START 0 -#define BOARD_SCORE_DISPLAY_START 2 #define BOARD_DISPLAY_START 4 #define BOARD_DISPLAY_END 9 #define MIN_CARD_VALUE 2 -#define MAX_CARD_VALUE 14 +#define MAX_CARD_VALUE KING #define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1) #define CARD_SUIT_COUNT 4 #define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT) @@ -111,7 +114,6 @@ static void shuffle_deck(void) { static void reset_deck(void) { current_card = 0; - stack_deck(); shuffle_deck(); } @@ -141,8 +143,8 @@ static void reset_board(bool first_round) { static void init_game(void) { watch_clear_display(); - watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START); - watch_display_string("GA", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT); + watch_display_text(WATCH_POSITION_TOP_LEFT, "HL"); reset_deck(); reset_board(true); score = 0; @@ -151,16 +153,23 @@ static void init_game(void) { } static void set_segment_at_position(segment_t segment, uint8_t position) { - const uint64_t position_segment_data = (Segment_Map[position] >> (8 * (uint8_t) segment)) & 0xFF; - const uint8_t com_pin = position_segment_data >> 6; - const uint8_t seg = position_segment_data & 0x3F; + digit_mapping_t segmap; + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) { + segmap = Custom_LCD_Display_Mapping[position]; + } else { + segmap = Classic_LCD_Display_Mapping[position]; + } + const uint8_t com_pin = segmap.segment[segment].address.com; + const uint8_t seg = segmap.segment[segment].address.seg; watch_set_pixel(com_pin, seg); } +static inline size_t get_display_position(size_t board_position) { + return FLIP_BOARD_DIRECTION ? BOARD_DISPLAY_START + board_position : BOARD_DISPLAY_END - board_position; +} + static void render_board_position(size_t board_position) { - const size_t display_position = FLIP_BOARD_DIRECTION - ? BOARD_DISPLAY_START + board_position - : BOARD_DISPLAY_END - board_position; + const size_t display_position = get_display_position(board_position); const bool revealed = game_board[board_position].revealed; //// Current position indicator spot @@ -178,18 +187,18 @@ static void render_board_position(size_t board_position) { const uint8_t value = game_board[board_position].value; switch (value) { - case 14: // A (≡) + case KING: // K (≡) watch_display_character(' ', display_position); set_segment_at_position(A, display_position); set_segment_at_position(D, display_position); set_segment_at_position(G, display_position); break; - case 13: // K (=) + case QUEEN: // Q (=) watch_display_character(' ', display_position); set_segment_at_position(A, display_position); set_segment_at_position(D, display_position); break; - case 12: // Q (-) + case JACK: // J (-) watch_display_character('-', display_position); break; default: { @@ -209,16 +218,16 @@ static void render_board_count(void) { // Render completed boards (screens) char buf[3] = {0}; snprintf(buf, sizeof(buf), "%2hhu", completed_board_count); - watch_display_string(buf, BOARD_SCORE_DISPLAY_START); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } static void render_final_score(void) { - watch_display_string("SC", STATUS_DISPLAY_START); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "SCORE", "SC "); char buf[7] = {0}; const uint8_t complete_boards = score / GUESSES_PER_SCREEN; snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score); watch_set_colon(); - watch_display_string(buf, BOARD_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } static guess_t get_answer(void) { @@ -251,13 +260,13 @@ static void do_game_loop(guess_t user_guess) { // Render answer indicator switch (answer) { case HL_GUESS_EQUAL: - watch_display_string("==", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_TOP_LEFT, "=="); break; case HL_GUESS_HIGHER: - watch_display_string("HI", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_TOP_LEFT, "HI"); break; case HL_GUESS_LOWER: - watch_display_string("LO", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_TOP_LEFT, "LO"); break; } @@ -268,18 +277,22 @@ static void do_game_loop(guess_t user_guess) { // No score for two consecutive identical cards } else { // Incorrect guess, game over - watch_display_string("GO", STATUS_DISPLAY_START); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "End", "GO"); game_board[guess_position].revealed = true; + watch_display_text(WATCH_POSITION_BOTTOM, "------"); + render_board_position(guess_position - 1); render_board_position(guess_position); + if (game_board[guess_position].value == JACK && guess_position < GAME_BOARD_SIZE) // Adds a space in case the revealed option is '-' + watch_display_character(' ', get_display_position(guess_position + 1)); game_state = HL_GS_LOSE; return; } if (score >= WIN_SCORE) { // Win, perhaps some kind of animation sequence? - watch_display_string("WI", STATUS_DISPLAY_START); - watch_display_string(" ", BOARD_SCORE_DISPLAY_START); - watch_display_string("------", BOARD_DISPLAY_START); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "WI"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WINNER", "winnEr"); game_state = HL_GS_WIN; return; } @@ -309,12 +322,12 @@ static void do_game_loop(guess_t user_guess) { break; case HL_GS_SHOW_SCORE: watch_clear_display(); - watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START); - watch_display_string("GA", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT); + watch_display_text(WATCH_POSITION_TOP_LEFT, "HL"); game_state = HL_GS_TITLE_SCREEN; break; default: - watch_display_string("ERROR", BOARD_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, "ERROR"); break; } } @@ -344,6 +357,7 @@ void higher_lower_game_face_activate(void *context) { (void) state; // Handle any tasks related to your watch face coming on screen. game_state = HL_GS_TITLE_SCREEN; + stack_deck(); } bool higher_lower_game_face_loop(movement_event_t event, void *context) { @@ -353,8 +367,8 @@ bool higher_lower_game_face_loop(movement_event_t event, void *context) { switch (event.event_type) { case EVENT_ACTIVATE: // Show your initial UI here. - watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START); - watch_display_string("GA", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT); + watch_display_text(WATCH_POSITION_TOP_LEFT, "HL"); break; case EVENT_TICK: // If needed, update your display here. diff --git a/legacy/watch_faces/complication/higher_lower_game_face.h b/watch-faces/complication/higher_lower_game_face.h similarity index 100% rename from legacy/watch_faces/complication/higher_lower_game_face.h rename to watch-faces/complication/higher_lower_game_face.h diff --git a/watch-faces/complication/interval_face.c b/watch-faces/complication/interval_face.c index 15593567..89f00b83 100644 --- a/watch-faces/complication/interval_face.c +++ b/watch-faces/complication/interval_face.c @@ -96,8 +96,7 @@ static inline void _inc_uint8(uint8_t *value, uint8_t step, uint8_t max) { static uint32_t _get_now_ts() { // returns the current date time as unix timestamp - watch_date_time_t now = watch_rtc_get_date_time(); - return watch_utility_date_time_to_unix_time(now, 0); + return movement_get_utc_timestamp(); } static inline void _button_beep() { diff --git a/watch-faces/complication/lander_face.c b/watch-faces/complication/lander_face.c new file mode 100644 index 00000000..8381ff0a --- /dev/null +++ b/watch-faces/complication/lander_face.c @@ -0,0 +1,577 @@ +/* + * MIT License + * + * Copyright (c) 2024 Klingon Jane + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// Emulator only: need time() to seed the random number generator. +#if __EMSCRIPTEN__ +#include +#endif + +#include +#include +#include +#include "lander_face.h" +#include "watch_common_display.h" + +#ifndef max +#define max(x, y) ((y) > (x) ? (y) : (x)) +#endif + +#ifndef min +#define min(x, y) ((x) > (y) ? (y) : (x)) +#endif + +#define LANDER_TICK_FREQUENCY 8 +#define MONSTER_DISPLAY_TICKS 9 +#define ENGINE_THRUST 11 +#define MODE_WAITING_TO_START 0 +#define MODE_DISPLAY_SKILL_LEVEL 1 +#define MODE_PLAYING 2 +#define MODE_TOUCHDOWN_BLANK 3 +#define MODE_DISPLAY_FINAL_STATUS 4 +#define MODE_MONSTER 5 +#define MODE_FIND_EARTH_MESSAGE 6 +#define CREWS_COMPLIMENT 13 +// Granularity is divisions per foot - height display +#define GRANUL 40 +// Next lines for repeat heroes only. +#define PROMOTION_INTERVAL 3 +#define LEVEL_ACE 8 +#define LEVEL_STARBUCK 11 +#define HARD_EARTH_INCREMENTS 11 +#define MAX_HARD_EARTH_CHANCE 6 + +// The gory final result calculations: +#define SPEED_FATALITY_ALL 41 +#define SPEED_FATALITY_NONE 26 +#define SPEED_NO_DAMAGE 21 +#define SPEED_LEVEL_INCREMENTS 2 +#define SPEED_MAJOR_CRASH 73 +#define MAJOR_CRASH_INCREMENTS 65 +#define SPEED_INJURY_NONE 20 +#define SPEED_INJURY_FULCRUM 32 +#define INJURY_FULCRUM_PROB 65 +#define FUEL_SCORE_GOOD 145 +#define FUEL_SCORE_GREAT 131 +#define FUEL_SCORE_FANTASTIC 125 + +// Joey Castillo to oversee storage allocation row +#define LANDER_STORAGE_ROW 2 +#define STORAGE_KEY_NUMBER 110 + +#define DIFFICULTY_LEVELS 3 +char lander_difficulty_names[DIFFICULTY_LEVELS][7] = { + "NOrMAL", "HArd ", "HArdEr" +}; +#define MONSTER_TYPES 4 +char lander_monster_names[MONSTER_TYPES][7] = { + "mOnStr", "6Erbil", "HAmStr", "Rabbit" +}; +#define MONSTER_ACTIONS 8 +char lander_monster_actions[MONSTER_ACTIONS][7] = { + "HUn6ry", " EAtS", "6Reedy", "annoYd", "nASty ", "SAVOry", "HO66SH", " pI66Y" +}; + +// -------------- +// Custom methods +// -------------- + + +static int gen_random_int (int16_t lower, int16_t upper) { + int range; + int retVal; + range = upper - lower + 1; + if ( range < 2 ) range = 2; + // Emulator: use rand. Hardware: use arc4random. + #if __EMSCRIPTEN__ + retVal = rand() % range; + #else + retVal = arc4random_uniform(range); + #endif + retVal += lower; + return retVal; +} + +static uint8_t assignProb ( uint8_t lowerProb, uint8_t upperProb, int16_t lowerSpeed, int16_t upperSpeed, int16_t actSpeed ) { + float probRange, speedRange; + float ratio, probFloat; + int probInt; + speedRange = upperSpeed - lowerSpeed; + if (speedRange<1.0) speedRange = 1.0; + probRange = upperProb - lowerProb; + ratio = ( (float) actSpeed - (float) lowerSpeed ) / speedRange; + probFloat = (float) lowerProb + ( ratio * probRange ); + probInt = (int) ( probFloat + 0.5 ); + probInt = min ( probInt, upperProb ); + probInt = max ( probInt, lowerProb ); + return (uint8_t) probInt; +} + +static void write_to_lander_EEPROM(lander_state_t *state) { + uint8_t output_array [ 3 ]; + output_array [ 0 ] = STORAGE_KEY_NUMBER; + output_array [ 1 ] = state->hero_counter; + output_array [ 2 ] = state->legend_counter; + watch_storage_erase ( LANDER_STORAGE_ROW ); + watch_storage_sync ( ); + watch_storage_write ( LANDER_STORAGE_ROW, 0, output_array, 3 ); +} + +// --------------------------- +// Standard watch face methods +// --------------------------- +void lander_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(lander_state_t)); + memset(*context_ptr, 0, sizeof(lander_state_t)); + lander_state_t *state = (lander_state_t *)*context_ptr; + state->led_enabled = false; + } + // Emulator only: Seed random number generator + #if __EMSCRIPTEN__ + srand(time(NULL)); + #endif +} + +void lander_face_activate(void *context) { + lander_state_t *state = (lander_state_t *)context; + char buf [ 7 ]; + state->mode = MODE_WAITING_TO_START; + state->led_active = false; + state->reset_counter = 0; + watch_clear_all_indicators ( ); + uint32_t offset = 0; + uint32_t size = 3; + uint8_t stored_data [ size ]; + // See if the hero_counter was ever written to EEPROM storage + watch_storage_read (LANDER_STORAGE_ROW, offset, stored_data, size); + if (stored_data[0] == STORAGE_KEY_NUMBER ) + { + state->hero_counter = stored_data [1]; // There's real data in there. + state->legend_counter = stored_data [2]; + } + else + { + state->hero_counter = 0; // Nope. Nothing there. + state->legend_counter = 0; + write_to_lander_EEPROM(state); // Initial EEPROM tracking data. + } + state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL; + state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit + // Fancy intro + if ( state->legend_counter == 0 ) watch_display_text(WATCH_POSITION_TOP_LEFT, "LA"); + else watch_display_text(WATCH_POSITION_TOP_LEFT, "LE"); + if ( ( state->hero_counter == 0 ) || ( state->hero_counter >= 40 ) ) watch_display_text ( WATCH_POSITION_TOP_RIGHT, " "); + else + { + sprintf ( buf, "%2d", state->hero_counter ); + watch_display_text ( WATCH_POSITION_TOP_RIGHT, buf); + } + if ( state->hero_counter >= 100 ) sprintf ( buf, "Str%3d", state->hero_counter ); + else if ( state->hero_counter >= 40 ) sprintf ( buf, "Strb%2d", state->hero_counter ); + else if ( state->hero_counter >= LEVEL_STARBUCK ) sprintf ( buf, "StrbUC" ); + else if ( state->hero_counter >= LEVEL_ACE ) sprintf ( buf, " ACE " ); // This human is good + else if ( state->difficulty_level == 0 ) sprintf ( buf, " " ); + else sprintf ( buf, "%s", lander_difficulty_names[state->difficulty_level] ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf); + if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL); + else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); +} + +bool lander_face_loop(movement_event_t event, void *context) { + lander_state_t *state = (lander_state_t *)context; + char buf [ 20 ]; // [11] is more correct and works; compiler too helpful. + + switch (event.event_type) { + case EVENT_TICK: + state->tick_counter++; + if ( state->mode == MODE_PLAYING ) { + int16_t accel = state->gravity; + bool gas_pedal_on = HAL_GPIO_BTN_ALARM_read() || HAL_GPIO_BTN_LIGHT_read(); + if ( gas_pedal_on && ( state->fuel_remaining > 0 ) ) { + accel = ENGINE_THRUST + state->gravity; // Gravity is negative + state->fuel_remaining--; // Used 1 fuel unit + watch_set_indicator ( WATCH_INDICATOR_LAP ); + // Low fuel warning indicators + if ( state->fuel_remaining == ( 3 * LANDER_TICK_FREQUENCY ) ) { // 3 seconds of fuel left + watch_set_indicator ( WATCH_INDICATOR_SIGNAL ); + watch_set_indicator ( WATCH_INDICATOR_BELL ); + watch_set_indicator ( WATCH_INDICATOR_PM ); + watch_set_indicator ( WATCH_INDICATOR_24H ); + } + else if ( state->fuel_remaining == 0 ) { // 0 seconds of fuel left, empty! + watch_clear_all_indicators ( ); + } + } + else { + watch_clear_indicator ( WATCH_INDICATOR_LAP ); + } + state->speed += accel; + state->height += state->speed; + if ( state->height > 971 * 80 ) { // Escape height + watch_clear_all_indicators (); + watch_display_text( WATCH_POSITION_BOTTOM, "ESCAPE" ); + state->tick_counter = 0; + state->mode = MODE_WAITING_TO_START; + } + else if ( state->height <= 0 ) { // Touchdown + state->tick_counter = 0; + state->mode = MODE_TOUCHDOWN_BLANK; + } + else { + // Update height display + sprintf ( buf, "%4d", (int) ( state->height / GRANUL ) ); + watch_display_text( WATCH_POSITION_BOTTOM, buf ); + } + } + else if ( state->mode == MODE_TOUCHDOWN_BLANK ) { + // Blank display on touchdown + if ( state->tick_counter == 1 ) { + watch_clear_all_indicators (); + watch_display_text( WATCH_POSITION_BOTTOM, " " ); + + // Also calc fuel score now. + float fuel_score_float; + uint16_t fuel_used; + fuel_used = state->fuel_start - state->fuel_remaining; + fuel_score_float = (float) fuel_used / (float) state->fuel_tpl; + state->fuel_score = (int) (fuel_score_float * 100.0 + 0.5); + if ( state->legend_counter == 0 ) state->fuel_score -= 8; // First Earth is easier + // Monitor reset_counter + if ( fuel_used >= 1 ) state->reset_counter = 0; + else state->reset_counter++; + if ( state->reset_counter >= 3 ) { + state->hero_counter = 0; + state->difficulty_level = 0; + if ( state->reset_counter >= 6 ) state->legend_counter = 0; + watch_display_text(WATCH_POSITION_BOTTOM, "rESET "); + write_to_lander_EEPROM(state); + } + } + // Wait until time for next display + if ( state->tick_counter >= ( 1 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + state->mode = MODE_DISPLAY_FINAL_STATUS; + } + } + else if ( state->mode == MODE_DISPLAY_FINAL_STATUS ) { + bool last_pass = false; + if ( state->tick_counter >= LANDER_TICK_FREQUENCY ) last_pass = true; + + // Show final status + if ( state->tick_counter == 1 ) { + // Calculate many attributes + // 1) Major crash: bug, crater, vaporized (gone). + // 2) Rank ship's health 0 to 8 + // 3) Crew fatalities and injuries + // 4) Special conditions: hero + // 5) Set fuel conservation indicators as appropriate + // 6) Set coffee maker OK indicator as appropriate + // 7) Green light if ship intact + // 8) Set standard display if not preempted. + bool allDone; + int16_t finalSpeed, boostedSpeed, levelsDamage; + int8_t shipsHealth, myRand; + uint8_t fatalities, probFatal, probInjury; + uint8_t i; + + allDone = false; + // Easiest implementation for difficulty_level is to increase touchdown speed above actual. + finalSpeed = abs ( state->speed ) + state->difficulty_level * 4; + // First Earth is a bit easier than all the others + if ( state->legend_counter == 0 ) finalSpeed -= 2; + + // 1) Major crash: bug, crater, vaporized (gone). + if ( finalSpeed >= SPEED_MAJOR_CRASH ) { + allDone = true; + shipsHealth = -1; + if ( finalSpeed >= ( SPEED_MAJOR_CRASH + 2 * MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, "GOnE " ); + else if ( finalSpeed >= ( SPEED_MAJOR_CRASH + MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, " CrAtr" ); + else sprintf ( buf, " bU6" ); + } + // 2) Rank ship's health 0 to 8 + if (!allDone) { + boostedSpeed = finalSpeed + SPEED_LEVEL_INCREMENTS - 1; + levelsDamage = (int) ( ( boostedSpeed - SPEED_NO_DAMAGE ) / SPEED_LEVEL_INCREMENTS ); + shipsHealth = 8 - levelsDamage; + shipsHealth = min ( shipsHealth, 8 ); // Keep between 0 and 8 + shipsHealth = max ( shipsHealth, 0 ); + } + state->ships_health = shipsHealth; // Remember ships health + // 3) Crew fatalities and injuries + if (!allDone) { + // Fatalies + probFatal = assignProb ( 0, 92, SPEED_FATALITY_NONE, SPEED_FATALITY_ALL, finalSpeed ); + // Injuries + if ( finalSpeed <= SPEED_INJURY_FULCRUM ) { + probInjury = assignProb ( 0, INJURY_FULCRUM_PROB, SPEED_INJURY_NONE, SPEED_INJURY_FULCRUM, finalSpeed ); + } else { + probInjury = assignProb ( INJURY_FULCRUM_PROB, 96, SPEED_INJURY_FULCRUM, SPEED_FATALITY_ALL, finalSpeed ); + } + fatalities = 0; + state->injured = 0; + for ( i = 0; i < CREWS_COMPLIMENT; i++ ) { + myRand = gen_random_int ( 1, 100 ); + if ( myRand <= probFatal ) fatalities++; + else if ( myRand <= probInjury ) state->injured++; + } + state->uninjured = CREWS_COMPLIMENT - fatalities - state->injured; + } + // 4) Special conditions: hero + if (!allDone) { + if ( (shipsHealth>=8) && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) { + state->hero_counter++; + if ( state->hero_counter==1 ) sprintf ( buf, "HErO " ); + else if ( state->hero_counter == LEVEL_ACE ) sprintf ( buf, " ACE " ); + else if ( state->hero_counter == LEVEL_STARBUCK ) sprintf ( buf, "STrbUC" ); + else if ( state->hero_counter>99 ) sprintf ( buf, "HEr%3d", state->hero_counter ); + else sprintf ( buf, "HErO%2d", state->hero_counter ); // Typical case + allDone = true; + // Two rule sets for finding Earth. Alternate between easy and hard. + int8_t my_odds, temp; + if ( state->legend_counter %2 == 0 ) my_odds = (int8_t) state->hero_counter - LEVEL_STARBUCK; // Easy + else { + temp = ( state->hero_counter - LEVEL_STARBUCK ) + HARD_EARTH_INCREMENTS - 1; + my_odds = temp / HARD_EARTH_INCREMENTS; + my_odds = min ( my_odds, MAX_HARD_EARTH_CHANCE ); + } + // Display odds in weekday region if positive value + if ( my_odds > 0 ) { + char buff3 [ 5 ]; + sprintf ( buff3, "%2d", my_odds ); + watch_display_text( WATCH_POSITION_TOP_RIGHT, buff3 ); + } else watch_display_text( WATCH_POSITION_TOP_RIGHT, " " ); + if ( my_odds >= gen_random_int ( 1, 200 ) ) { // EARTH!!!! The final objective. + sprintf ( buf, "EArTH " ); // 17% within 8, 50% by 16, 79% by 24, 94% by 32 <- easy mode + state->hero_counter = 0; + state->legend_counter++; + } + // Recalculate difficulty level base on new hero_counter. + state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL; + state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit + // Write to EEPROM + write_to_lander_EEPROM(state); + } + } + // 5) Set fuel conservation indicators as appropriate + if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) watch_set_indicator ( WATCH_INDICATOR_LAP ); + if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GREAT ) ) watch_set_indicator ( WATCH_INDICATOR_24H ); + if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GOOD ) ) watch_set_indicator ( WATCH_INDICATOR_PM ); + // 6) Set coffee maker OK indicator as appropriate + if ( shipsHealth >= 5 || ( shipsHealth >= 0 && ( gen_random_int ( 0, 3 ) != 1 ) ) ){ + watch_set_indicator ( WATCH_INDICATOR_SIGNAL ); + } + // 7) Green light if ship intact + if ( shipsHealth >= 8 && state->led_enabled) { + watch_set_led_green ( ); + state->led_active = true; + } + // 8) Set standard display if not preempted. + if (!allDone) { + if ( ( state->injured > 0 ) || ( state->uninjured == 0 ) ) { + sprintf ( buf, "%d %2d%2d", shipsHealth, state->uninjured, state->injured ); + } + else { + sprintf ( buf, "%d %2d ", shipsHealth, state->uninjured ); + } + } + // Display final status. + watch_display_text(WATCH_POSITION_BOTTOM, buf ); + } // End if tick_counter == 1 + + // Major crash - ship burning with red LED. + if ( state->ships_health < 0 && state->led_enabled) { + if ( ( gen_random_int ( 0, 1 ) != 1 ) && !last_pass ) { // Always off on last pass + // Turn on red LED. + watch_set_led_red ( ); + state->led_active = true; + } else { + watch_set_led_off ( ); + } + } + // Wait long enough, then allow waiting for next game. + if ( last_pass ) { + watch_set_led_off ( ); + // No change to display text, allow new game to start. + state->mode = MODE_WAITING_TO_START; + // Unless it's time for monsters + uint8_t survivors = state->injured + state->uninjured; + if ( ( state->ships_health >= 0 ) && ( survivors > 0 ) && + ( gen_random_int ( -1, 3 ) >= state->ships_health ) ) { + state->mode = MODE_MONSTER; + state->tick_counter = 0; + state->monster_type = gen_random_int ( 0, MONSTER_TYPES - 1 ); + } + } + } // End if MODE_DISPLAY_FINAL_STATUS + else if ( state->mode == MODE_DISPLAY_SKILL_LEVEL ) { + // Display skill level + if ( state->tick_counter == 1 ) { + sprintf ( buf, " %d", state->skill_level ); + watch_display_text ( WATCH_POSITION_TOP_RIGHT, buf ); + sprintf ( buf, " %d ", state->skill_level ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf ); + } + // Wait long enough, then start game. + if ( state->tick_counter >= ( 2.0 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + // Houston, WE ARE LAUNCHING NOW.... + state->mode = MODE_PLAYING; + } + } + else if ( state->mode == MODE_FIND_EARTH_MESSAGE ) { + // Display "Find" then "Earth" + if ( state->tick_counter == 1 ) { + sprintf ( buf, " FInd " ); + watch_display_text ( WATCH_POSITION_TOP_RIGHT, " " ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf ); + } + if ( state->tick_counter == (int) ( 1.5 * LANDER_TICK_FREQUENCY + 1 ) ) { + sprintf ( buf, "EArTH " ); + watch_display_text ( WATCH_POSITION_TOP_RIGHT, " " ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf ); + } + // Wait long enough, then display skill level. + if ( state->tick_counter >= ( 3 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + state->mode = MODE_DISPLAY_SKILL_LEVEL; + } + } + else if ( state->mode == MODE_MONSTER ) { + if ( state->tick_counter == 1 ) watch_display_text ( WATCH_POSITION_BOTTOM, lander_monster_names[state->monster_type] ); + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS + 1 ) { + uint8_t my_rand; + my_rand = gen_random_int ( 0 , MONSTER_ACTIONS - 1 ); + watch_display_text ( WATCH_POSITION_BOTTOM, lander_monster_actions[my_rand] ); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 ) { // Display 1st monster character + sprintf ( buf, "%s", lander_monster_names[state->monster_type] ); + buf [1] = 0; + watch_display_text(WATCH_POSITION_BOTTOM, buf); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 1 ) { // Display current population, close mouth + sprintf ( buf, " c%2d%2d", state->uninjured, state->injured ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf ); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 3 ) watch_display_character ( 'C', 5 ); // Open mouth + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 5 ) { + // Decision to: continue loop, end loop or eat astronaut + uint8_t survivors = state->injured + state->uninjured; + uint8_t myRand = gen_random_int ( 0, 16 ); + if ( survivors == 0 ) state->mode = MODE_WAITING_TO_START; + else if ( myRand <= 1 ) { // Leave loop with survivors + sprintf ( buf, "%d %2d%2d", state->ships_health, state->uninjured, state->injured ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf); + state->mode = MODE_WAITING_TO_START; + } else if ( myRand <= 11 ) state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Do nothing, loop continues + else { // Eat an astronaut - welcome to the space program! + if ( state->injured > 0 && state->uninjured > 0 ) { + if ( gen_random_int ( 0,1 ) == 0 ) state->injured--; + else state->uninjured--; + } + else if ( state->injured > 0 ) state->injured--; + else state->uninjured--; + state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Re-display + } + } + else if ( state->tick_counter >= MONSTER_DISPLAY_TICKS * 4 ) state->mode = MODE_WAITING_TO_START; // Safety + } // End if MODE_MONSTER + break; // End case EVENT_TICK + case EVENT_ALARM_BUTTON_DOWN: + if ( state->mode == MODE_WAITING_TO_START ) { + // That was the go signal - start a new game!! + float numerator, denominator, timeSquared; + int16_t gravity, thrust; + float myTime, distToTop, fuel_mult; + uint8_t skill_level; + int32_t tplTop; // Top lander height for TPL calculations + movement_request_tick_frequency(LANDER_TICK_FREQUENCY); + watch_set_led_off ( ); // Safety + watch_clear_all_indicators ( ); + // Randomize starting parameters + state->height = gen_random_int ( 131, 181 ) * 80; + // Per line below; see Mars Orbiter September 23, 1999 + if ( gen_random_int ( 0, 8 ) == 5 ) state->height = gen_random_int ( 240, 800 ) * 80; + state->speed = gen_random_int ( -120, 35 ); // Positive is up + state->gravity = gen_random_int ( -3, -2 ) * 2; // negative downwards value + skill_level = gen_random_int ( 1, 4 ); // Precursor to fuel allocation + // Theoretical Perfect Landing (TPL) calculations start here. + myTime = (float) state->speed / (float) state->gravity; // How long to reach this speed? Don't care which way sign is. + distToTop = fabs ( 0.5 * state->gravity * myTime * myTime ); + tplTop = (int) ( state->height + distToTop + 0.5 ); // Theoretical highest point based on all of speed, height and gravity. + // Time squared = ( 2 * grav * height ) / ( t*t + g*t ), where t is net acceleration with thrust on. + gravity = abs ( state->gravity ); + thrust = ENGINE_THRUST + state->gravity; + numerator = 2.0 * (float) gravity * (float) tplTop; + denominator = thrust * thrust + thrust * gravity; + timeSquared = numerator / denominator; + state->fuel_tpl = (int) ( sqrt ( timeSquared ) + 0.5 ); // Fuel required for theoretical perfect landing (TPL). + if ( skill_level == 1 ) fuel_mult = 4.0; // TPL + 300% + else if ( skill_level == 2 ) fuel_mult = 2.5; // TPL + 150% + else if ( skill_level == 3 ) fuel_mult = 1.6; // TPL + 60% + else fuel_mult = 1.3; // TPL + 30% + state->fuel_start = state->fuel_tpl * fuel_mult; + state->fuel_remaining = state->fuel_start; + state->skill_level = skill_level; + state->tick_counter = 0; + if ( gen_random_int ( 1, 109 ) != 37 ) { + // Houston, approaching launch.... + state->mode = MODE_DISPLAY_SKILL_LEVEL; + } + else state->mode = MODE_FIND_EARTH_MESSAGE; + } + break; + case EVENT_LIGHT_BUTTON_DOWN: + if ( state->mode == MODE_WAITING_TO_START ) { + // Display difficulty level + watch_display_text(WATCH_POSITION_BOTTOM, lander_difficulty_names [state->difficulty_level]); + } + break; + case EVENT_LIGHT_LONG_PRESS: + if ( state->mode != MODE_WAITING_TO_START ) break; + state->led_enabled = !state->led_enabled; + if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL); + else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + break; + case EVENT_LIGHT_LONG_UP: + if ( ( state->mode == MODE_WAITING_TO_START ) && ( state->legend_counter > 0 ) ) { + if ( state->legend_counter > 9 ) sprintf (buf,"EArt%2d", state->legend_counter ); + else sprintf (buf,"EArth%d", state->legend_counter ); + // Display legend counter + watch_display_text(WATCH_POSITION_BOTTOM, buf); + } + break; + + default: + movement_default_loop_handler(event); + break; + } + if ( !state->led_active ) return true; + else return false; +} + +void lander_face_resign(void *context) { + (void) context; + watch_set_led_off ( ); +} \ No newline at end of file diff --git a/watch-faces/complication/lander_face.h b/watch-faces/complication/lander_face.h new file mode 100644 index 00000000..5e4c344f --- /dev/null +++ b/watch-faces/complication/lander_face.h @@ -0,0 +1,152 @@ +/* + * MIT License + * + * Copyright (c) 2024 Klingon Jane + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef LANDER_FACE_H_ +#define LANDER_FACE_H_ + +#include "movement.h" + +/* + +My remake of a classic planet landing game. + +Objective: Safely land the Cringeworthy. +Use your limited fuel supply to achieve a soft touch-down. + +End scenarios and ship's health: + +Hero They name this planet after you. +8 Life is very cozy. +7 +6 +5 Life is tolerable, plus some creature comforts +4 +3 Marooned. +2 +1 +0 Ship destroyed. Life is harsh, no shelter. Giant hamsters are cute. ** +Bug As in squished. +Crater They name this crater after you. +Gone As in vapourized. + +Landing display format is: +Ship's health, intact crewmen, injured crewmen. + +Additional data: +Crew's compliment: 13. +Low fuel warning icons: activates when 3 seconds of full thrust remains. +** Yes, hamsters are very cute. However; some eating of astronauts may occur. + +Starting velocity, height and gravity are randomized each scenario. +Fuel levels randomly assigned from 1 to 4 (hardest) to match starting parameters. + +A safe landing is always possible. + +End of game icons: +LAP - Fantastic budgeting of fuel supply ( Required for heroic landing status. ) +24H - Great budgeting of fuel supply +PM - Good budgeting of fuel supply +SIGNAL - The combination coffee and tea maker survived + +Landings get progressively harder with the number of heroic landings made. +Number of heroic landings are remembered. + +Heroic +Landings Status + 0 Normal + 3 Hard ( first difficulty increase ) + 6 Harder ( final difficulty increase ) + 8 Ace + 11 ?????? + +Save yourself. Save the coffee maker. + +END of standard training manual + +*/ + + +/* + +What is really going on here? +The fleet is lost. You are a newbie pilot making a name for yourself. + +Objective: Find Earth. + +After reaching ?????? status, future heroic sorties will have 'some' chance in 200 +of finding Earth. + +Your chances improve by 1 chance in 200 for each subsequent Heroic Landing (HL). + +Completing HL 12 will give you 1 chance in 200, for that landing. +HL 13 will give you 2 chances in 200, for that landing. +HL 14 will give you 3 chances in 200, for that landing. +HL 20 will give you 9 chances in 200, for that landing, and so on. + +At these higher levels, your chances in 200 are displayed in the upper right corner on a heroic landing. + +For wannabe pilots only: The HL counter can be reset by crashing three consecutive +missions without touching the thrust button. ( 6 to reset Earth-found counter ) + +Find Earth. Save Humanity. + +*/ + +typedef struct { + int32_t height; + int16_t speed; // Positive is up + uint16_t tick_counter; // For minimum delays + uint16_t fuel_start; + uint16_t fuel_remaining; + uint16_t fuel_tpl; // Fuel required for theoretical perfect landing + uint16_t fuel_score; // 100 is perfect; higher is less perfect + int8_t gravity; // negative downwards value + bool led_enabled; // Can the led be turned on? + bool led_active; // Did we use it this scenario? + uint8_t mode; // 0 Pre-launch waiting, 1 show level, 2 playing, 3 touchdown blank, 4 final display, 5 monster + uint8_t skill_level; // 1 thru 4. Dictates fuel alloted + int8_t ships_health; // 0 thru 8. -1 = major crash + uint8_t hero_counter; // Total heroic landings ever + uint8_t legend_counter; // Historic events counter ( Earth ) + uint8_t difficulty_level; // Based on hero_counter + uint8_t reset_counter; // Can reset hero_counter by crashing using zero fuel several consecutive scenarios + uint8_t monster_type; // Which monster is hungry? + uint8_t uninjured; // OK survivors + uint8_t injured; // Hurt survivors +} lander_state_t; + +void lander_face_setup(uint8_t watch_face_index, void ** context_ptr); +void lander_face_activate(void *context); +bool lander_face_loop(movement_event_t event, void *context); +void lander_face_resign(void *context); + +#define lander_face ((const watch_face_t){ \ + lander_face_setup, \ + lander_face_activate, \ + lander_face_loop, \ + lander_face_resign, \ + NULL, \ +}) + +#endif // LANDER_FACE_H_ diff --git a/watch-faces/complication/moon_phase_face.c b/watch-faces/complication/moon_phase_face.c index 9e9591d3..c7a550dc 100644 --- a/watch-faces/complication/moon_phase_face.c +++ b/watch-faces/complication/moon_phase_face.c @@ -184,10 +184,19 @@ bool moon_phase_face_loop(movement_event_t event, void *context) { state->offset += 86400; _update(state, state->offset); break; - case EVENT_ALARM_LONG_PRESS: - state->offset = 0; + case EVENT_ALARM_LONG_PRESS: + state->offset = 0; _update(state, state->offset); - break; + break; + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_LIGHT_BUTTON_UP: + state->offset -= 86400; + _update(state, state->offset); + break; + case EVENT_LIGHT_LONG_PRESS: + movement_illuminate_led(); + break; case EVENT_TIMEOUT: // QUESTION: Should timeout reset offset to 0? break; diff --git a/watch-faces/complication/moon_phase_face.h b/watch-faces/complication/moon_phase_face.h index b0209829..2a32ed6b 100644 --- a/watch-faces/complication/moon_phase_face.h +++ b/watch-faces/complication/moon_phase_face.h @@ -47,6 +47,9 @@ * each button press, and both the text and the graphical representation will * display the moon phase for that day. Try pressing the Alarm button 27 times * now, just to visualize what the moon will look like over the next month. + * Pressing the Light button will move back in time. + * + * Holding the Light button will illuminate the display. */ #include "movement.h" diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c new file mode 100644 index 00000000..a0591ba3 --- /dev/null +++ b/watch-faces/complication/ping_face.c @@ -0,0 +1,583 @@ +/* + * MIT License + * + * Copyright (c) 2024 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include "ping_face.h" +#include "delay.h" +#include "watch_common_display.h" + +typedef enum { + PADDLE_RETRACTED = 0, + PADDLE_EXTENDING, + PADDLE_EXTENDED, + PADDLE_RETRACTING, +} PingPaddleState; + +typedef enum { + SCREEN_TITLE = 0, + SCREEN_SCORE, + SCREEN_PLAYING, + SCREEN_LOSE, + SCREEN_COUNT +} PingCurrScreen; + +typedef enum { + DIFF_BABY = 0, // FREQ_BABY FPS + DIFF_EASY, // FREQ_EASY FPS + DIFF_NORM, // FREQ_NORM FPS + DIFF_HARD, // FREQ_NORM FPS, smaller travel-distance for ball + DIFF_COUNT +} PingDifficulty; + +typedef enum { + RESULT_LOSE = -1, + RESULT_NONE = 0, + RESULT_HIT = 1, + RESULT_FIRST_HIT = 2, +} PingResult; + +#define FREQ_BABY 2 +#define FREQ_EASY 4 +#define FREQ_NORM 8 + +#define BALL_POS_MAX 11 +#define BALL_OFF_SCREEN 100 +#define MAX_HI_SCORE 9999 // Max hi score to store and display on the title screen. +#define MAX_DISP_SCORE 39 // The top-right digits can't properly display above 39 + +typedef struct { + uint8_t ball_pos; // 0 to 11; 0 is the bottom-right and 11 is the top right. + // | 6 | 7 | 8 | 9 | 10 | 11 | + // | 5 | 4 | 3 | 2 | 1 | 0 | + PingPaddleState paddle_pos; + uint8_t ball_char_pos; // Derived from ball_pos + bool ball_is_clockwise; + bool ball_is_moving; + uint16_t curr_score; + PingCurrScreen curr_screen; + bool paddle_hit; + bool paddle_released; + uint8_t curr_freq; + bool moving_from_tap; +} game_state_t; + +static game_state_t game_state; +static int8_t _ticks_show_title = 0; +static bool _is_custom_lcd; + +static int8_t start_tune[] = { + BUZZER_NOTE_C5, 15, + BUZZER_NOTE_E5, 15, + BUZZER_NOTE_G5, 15, + 0 +}; + +static int8_t lose_tune[] = { + BUZZER_NOTE_D3, 10, + BUZZER_NOTE_C3SHARP_D3FLAT, 10, + BUZZER_NOTE_C3, 10, + 0 +}; + +static uint8_t ball_pos_to_char_pos(uint8_t ball_pos) { + switch (ball_pos) + { + case 5: + case 6: + return 4; + case 4: + case 7: + return 5; + case 3: + case 8: + return 6; + case 2: + case 9: + return 7; + case 1: + case 10: + return 8; + case 0: + case 11: + return 9; + default: + return BALL_OFF_SCREEN; + } +} + +static bool paddle_and_ball_on_same_segment(void) { + if (game_state.paddle_pos == PADDLE_EXTENDED) { + if (game_state.ball_pos == 9 || game_state.ball_pos == 2) { + return true; + } + } + else if (game_state.paddle_pos == PADDLE_EXTENDING || game_state.paddle_pos == PADDLE_RETRACTING) { + if (game_state.ball_pos == 10 || game_state.ball_pos == 1) { + return true; + } + } + else if (game_state.paddle_pos == PADDLE_RETRACTED) { + if (game_state.ball_pos == 11 || game_state.ball_pos == 0) { + return true; + } + } + return false; +} + +static bool paddle_hit_ball(void) { + if (game_state.paddle_pos == PADDLE_EXTENDED) { + if (game_state.ball_pos >= 9 && game_state.ball_is_clockwise) { + return true; + } + if (game_state.ball_pos <= 2 && !game_state.ball_is_clockwise) { + return true; + } + } + else if (game_state.paddle_pos == PADDLE_EXTENDING) { + if (game_state.ball_pos >= 10 && game_state.ball_is_clockwise) { + return true; + } + if (game_state.ball_pos <= 1 && !game_state.ball_is_clockwise) { + return true; + } + } + return false; +} + +static uint8_t get_next_ball_pos(bool ball_hit, uint8_t difficulty) { + int8_t offset_next; + if (ball_hit) { + bool ball_on_top = game_state.ball_pos > 5; + game_state.ball_is_clockwise = !ball_on_top; + // ball is at the same frame as the paddle + if (game_state.paddle_pos == PADDLE_EXTENDED) { + return ball_on_top ? 9 : 2; + } else if (game_state.paddle_pos == PADDLE_EXTENDING) { + return ball_on_top ? 10 : 1; + } + } + if (game_state.ball_is_clockwise) { + offset_next = 1; + } else { + offset_next = -1; + } + int8_t next_pos = game_state.ball_pos + offset_next; + if (next_pos > BALL_POS_MAX || next_pos < 0) { + return BALL_OFF_SCREEN; + } + if (difficulty == DIFF_HARD) { + if (next_pos == 4) { + next_pos = 8; + } else if (next_pos == 7) { + next_pos = 3; + } + } + return next_pos; +} + +static void display_ball(void) { + uint8_t char_pos = ball_pos_to_char_pos(game_state.ball_pos); + uint8_t char_display; + bool overlap = paddle_and_ball_on_same_segment(); + if (game_state.ball_pos > 5) { + if (overlap) { + char_display = 'q'; + } else { + char_display = '#'; + } + } else { + if (!_is_custom_lcd && (char_pos == 4 || char_pos == 6)) { + char_display = 'n'; // No need to check for overlap on these segments + } else { + if (overlap) { + char_display = 'd'; + } else { + char_display = 'o'; + } + } + } + watch_display_character(char_display, char_pos); +} + +static PingResult update_ball(uint8_t difficulty) { + bool ball_hit = paddle_hit_ball(); + bool first_hit = false; + if (!game_state.ball_is_moving) { + if (ball_hit) { + game_state.ball_is_moving = true; + first_hit = true; + } else { + return RESULT_NONE; + } + } + game_state.ball_pos = get_next_ball_pos(ball_hit, difficulty); + if (game_state.ball_pos == BALL_OFF_SCREEN) { + return RESULT_LOSE; + } + display_ball(); + if (ball_hit) { + return first_hit ? RESULT_FIRST_HIT : RESULT_HIT; + } else { + return RESULT_NONE; + } +} + +static void display_paddle(void) { + switch (game_state.paddle_pos) + { + case PADDLE_EXTENDING: + case PADDLE_RETRACTING: + watch_display_character('-', 9); + watch_display_character('1', 8); + break; + case PADDLE_EXTENDED: + watch_display_character('-', 9); + watch_display_character('-', 8); + watch_display_character('1', 7); + break; + case PADDLE_RETRACTED: + default: + watch_display_character('1', 9); + break; + } +} + +static void update_paddle(void) { + switch (game_state.paddle_pos) + { + case PADDLE_RETRACTED: + if (game_state.paddle_hit) { + game_state.paddle_pos = PADDLE_EXTENDING; + } + break; + case PADDLE_EXTENDING: + if (!game_state.moving_from_tap && !HAL_GPIO_BTN_ALARM_read()) { + game_state.paddle_pos = PADDLE_RETRACTED; + watch_display_character(' ', 8); + game_state.moving_from_tap = false; + } else { + game_state.paddle_pos = PADDLE_EXTENDED; + } + break; + case PADDLE_EXTENDED: + game_state.paddle_pos = PADDLE_RETRACTING; + watch_display_character(' ', 7); + break; + case PADDLE_RETRACTING: + game_state.paddle_pos = PADDLE_RETRACTED; + watch_display_character(' ', 8); + game_state.moving_from_tap = false; + break; + default: + break; + } + game_state.paddle_hit = false; + display_paddle(); +} + +static inline bool paddle_is_extending(void) { + return game_state.paddle_pos == PADDLE_EXTENDING || game_state.paddle_pos == PADDLE_EXTENDED; +} + +static void display_score(uint8_t score) { + char buf[3]; + score %= (MAX_DISP_SCORE + 1); + sprintf(buf, "%2d", score); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); +} + +static void add_to_score(ping_state_t *state) { + if (game_state.curr_score <= MAX_HI_SCORE) { + game_state.curr_score++; + if (game_state.curr_score > state -> hi_score) + state -> hi_score = game_state.curr_score; + } + display_score(game_state.curr_score); +} + +static void check_and_reset_hi_score(ping_state_t *state) { + // Resets the hi score at the beginning of each month. + 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)) + { + // The high score resets itself every new month. + state -> hi_score = 0; + state -> year_last_hi_score = date_time.unit.year; + state -> month_last_hi_score = date_time.unit.month; + } +} + +static void display_difficulty(uint16_t difficulty) { + static const char *labels[] = { + [DIFF_BABY] = " b", + [DIFF_EASY] = " E", + [DIFF_NORM] = " N", + [DIFF_HARD] = " H" + }; + watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]); +} + +static void change_difficulty(ping_state_t *state) { + state -> difficulty = (state -> difficulty + 1) % DIFF_COUNT; + display_difficulty(state -> difficulty); + if (state -> soundOn) { + if (state -> difficulty == 0) watch_buzzer_play_note(BUZZER_NOTE_B4, 30); + else watch_buzzer_play_note(BUZZER_NOTE_C5, 30); + } +} + +static void display_sound_indicator(bool soundOn) { + if (soundOn) { + watch_set_indicator(WATCH_INDICATOR_BELL); + } else { + watch_clear_indicator(WATCH_INDICATOR_BELL); + } +} + +static void toggle_sound(ping_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(ping_state_t *state) { + if (!state->tap_control_on) { + movement_enable_tap_detection_if_available(); + state->tap_control_on = true; + } +} + +static void disable_tap_control(ping_state_t *state) { + if (state->tap_control_on) { + movement_disable_tap_detection_if_available(); + state->tap_control_on = false; + } +} + +static void display_title(ping_state_t *state) { + movement_request_tick_frequency(1); + game_state.curr_screen = SCREEN_TITLE; + watch_clear_colon(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "Ping", "PI "); + watch_display_text(WATCH_POSITION_BOTTOM, " Ping "); + display_sound_indicator(state -> soundOn); + _ticks_show_title = 1; +} + +static void display_score_screen(ping_state_t *state) { + uint16_t hi_score = state -> hi_score; + uint8_t difficulty = state -> difficulty; + movement_request_tick_frequency(1); + bool sound_on = state -> soundOn; + memset(&game_state, 0, sizeof(game_state)); + game_state.curr_screen = SCREEN_SCORE; + watch_set_colon(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "PI ", "PI "); + if (hi_score > MAX_HI_SCORE) { + watch_display_text(WATCH_POSITION_BOTTOM, "HS --"); + } + else { + 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 begin_playing(ping_state_t *state) { + game_state.curr_screen = SCREEN_PLAYING; + watch_clear_colon(); + display_sound_indicator(state -> soundOn); + switch (state -> difficulty) + { + case DIFF_BABY: + game_state.curr_freq = FREQ_BABY; + break; + case DIFF_EASY: + game_state.curr_freq = FREQ_EASY; + break; + case DIFF_NORM: + case DIFF_HARD: + default: + game_state.curr_freq = FREQ_NORM; + break; + } + movement_request_tick_frequency(game_state.curr_freq); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, " "); + game_state.paddle_pos = PADDLE_RETRACTED; + game_state.ball_pos = 1; + game_state.paddle_hit = false; + game_state.ball_is_moving = false; + game_state.ball_is_clockwise = false; + game_state.curr_score = 0; + display_paddle(); + display_ball(); + display_score( game_state.curr_score); +} + +static void display_lose_screen(ping_state_t *state) { + game_state.curr_screen = SCREEN_LOSE; + game_state.curr_score = 0; + 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 update_game(ping_state_t *state) { + if (game_state.ball_is_moving) { + watch_display_character(' ', ball_pos_to_char_pos(game_state.ball_pos)); // remove the old ball. + } + update_paddle(); + int game_result = update_ball(state -> difficulty); + if (game_result == RESULT_LOSE) { + display_lose_screen(state); + } else if (game_result == RESULT_HIT) { + add_to_score(state); + if (state -> soundOn) { + watch_buzzer_play_note(BUZZER_NOTE_C5, 60); + } + } else if (game_result == RESULT_FIRST_HIT && state -> soundOn) { + watch_buzzer_play_sequence(start_tune, NULL); + } +} + +void ping_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(ping_state_t)); + memset(*context_ptr, 0, sizeof(ping_state_t)); + ping_state_t *state = (ping_state_t *)*context_ptr; + state->difficulty = DIFF_NORM; + state->tap_control_on = false; + } +} + +void ping_face_activate(void *context) { + (void) context; + _is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; + if (watch_sleep_animation_is_running()) { + watch_stop_blink(); + } +} + +bool ping_face_loop(movement_event_t event, void *context) { + ping_state_t *state = (ping_state_t *)context; + switch (event.event_type) { + case EVENT_ACTIVATE: + disable_tap_control(state); + check_and_reset_hi_score(state); + display_title(state); + break; + case EVENT_TICK: + switch (game_state.curr_screen) + { + case SCREEN_TITLE: + if (_ticks_show_title > 0) {_ticks_show_title--;} + else { + watch_clear_display(); + display_score_screen(state); + } + case SCREEN_SCORE: + case SCREEN_LOSE: + break; + case SCREEN_PLAYING: + default: + update_game(state); + break; + } + break; + case EVENT_ALARM_BUTTON_UP: + case EVENT_LIGHT_BUTTON_UP: + 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_LOSE: + watch_clear_display(); + display_score_screen(state); + default: + break; + } + break; + case EVENT_LIGHT_LONG_PRESS: + if (game_state.curr_screen == SCREEN_SCORE) + change_difficulty(state); + break; + case EVENT_SINGLE_TAP: + case EVENT_DOUBLE_TAP: + // 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; + } + else if (game_state.curr_screen == SCREEN_PLAYING) { + game_state.moving_from_tap = true; + game_state.paddle_hit = true; + } + break; + case EVENT_ALARM_BUTTON_DOWN: + if (game_state.curr_screen == SCREEN_PLAYING) { + game_state.moving_from_tap = false; + game_state.paddle_hit = true; + } + break; + case EVENT_ALARM_LONG_PRESS: + if (game_state.curr_screen == SCREEN_TITLE || game_state.curr_screen == SCREEN_SCORE) + toggle_sound(state); + break; + case EVENT_TIMEOUT: + disable_tap_control(state); + if (game_state.curr_screen != SCREEN_SCORE) { + display_score_screen(state); + } + break; + case EVENT_LIGHT_BUTTON_DOWN: + break; + default: + return movement_default_loop_handler(event); + } + return true; +} + +void ping_face_resign(void *context) { + ping_state_t *state = (ping_state_t *)context; + disable_tap_control(state); +} diff --git a/watch-faces/complication/ping_face.h b/watch-faces/complication/ping_face.h new file mode 100644 index 00000000..2207bec3 --- /dev/null +++ b/watch-faces/complication/ping_face.h @@ -0,0 +1,71 @@ +/* + * MIT License + * + * Copyright (c) 2025 + * + * 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 PING_FACE_H_ +#define PING_FACE_H_ + +#include "movement.h" + +/* + PING face + I saw the face made on the Ollee watch and thought it'd be fun to have on my Sensorwatch. + https://www.instagram.com/reel/DNlTb-ERE1F/ + On the title screen, you can select a difficulty by long-pressing LIGHT or toggle sound by long-pressing ALARM. + ALARM are used to paddle. Holding the ALARM button longer makes the paddle travel further. + If the accelerometer is installed, you can tap the screen to move the paddle. Paddle will travel its full distance when tapping is used. + High-score is displayed on the top-right on the title screen. During a game, the current score is displayed. + + Difficulties: + Baby: 2 FPS + Easy: 4 FPS + Normal: 8 FPS + Hard: 8 FPS and the ball travels half the half the board. + +*/ + +typedef struct { + uint16_t hi_score : 10; + uint8_t difficulty : 3; + uint8_t month_last_hi_score : 4; + uint8_t year_last_hi_score : 6; + uint8_t soundOn : 1; + uint8_t tap_control_on : 1; + uint8_t unused : 7; +} ping_state_t; + +void ping_face_setup(uint8_t watch_face_index, void ** context_ptr); +void ping_face_activate(void *context); +bool ping_face_loop(movement_event_t event, void *context); +void ping_face_resign(void *context); + +#define ping_face ((const watch_face_t){ \ + ping_face_setup, \ + ping_face_activate, \ + ping_face_loop, \ + ping_face_resign, \ + NULL, \ +}) + +#endif // ping_FACE_H_ + diff --git a/watch-faces/complication/pulsometer_face.c b/watch-faces/complication/pulsometer_face.c index 42f445af..dcef39c9 100644 --- a/watch-faces/complication/pulsometer_face.c +++ b/watch-faces/complication/pulsometer_face.c @@ -80,6 +80,7 @@ static void pulsometer_display_measurement(pulsometer_state_t *pulsometer) { char buf[5]; int16_t value = pulsometer->pulses; + if (value < 0) value = 0; if (value > 9999) value = 9999; snprintf(buf, sizeof(buf), "%-4hd", value); diff --git a/legacy/watch_faces/complication/simon_face.c b/watch-faces/complication/simon_face.c similarity index 85% rename from legacy/watch_faces/complication/simon_face.c rename to watch-faces/complication/simon_face.c index 41bc5c3d..7b54f12f 100644 --- a/legacy/watch_faces/complication/simon_face.c +++ b/watch-faces/complication/simon_face.c @@ -23,6 +23,7 @@ */ #include "simon_face.h" +#include "delay.h" #include #include #include @@ -47,18 +48,19 @@ static inline uint8_t _simon_get_rand_num(uint8_t num_values) { } static void _simon_clear_display(simon_state_t *state) { - if (state->playing_state == SIMON_NOT_PLAYING) { - watch_display_string(" ", 0); - } else { - sprintf(_simon_display_buf, " %2d ", state->sequence_length); - watch_display_string(_simon_display_buf, 0); + watch_clear_display(); + if (state->playing_state != SIMON_NOT_PLAYING) { + sprintf(_simon_display_buf, "%2d", state->sequence_length); + watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf); } } static void _simon_not_playing_display(simon_state_t *state) { _simon_clear_display(state); - sprintf(_simon_display_buf, "SI %d", state->best_score); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "SIMON", "SI"); + sprintf(_simon_display_buf, "%d", state->best_score); + watch_display_text(WATCH_POSITION_BOTTOM, _simon_display_buf); if (!state->soundOff) watch_set_indicator(WATCH_INDICATOR_BELL); else @@ -67,14 +69,13 @@ static void _simon_not_playing_display(simon_state_t *state) { watch_set_indicator(WATCH_INDICATOR_SIGNAL); else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); - watch_display_string(_simon_display_buf, 0); switch (state->mode) { case SIMON_MODE_EASY: - watch_display_string("E", 9); + watch_display_text(WATCH_POSITION_SECONDS, " E"); break; case SIMON_MODE_HARD: - watch_display_string("H", 9); + watch_display_text(WATCH_POSITION_SECONDS, " H"); break; default: break; @@ -90,24 +91,27 @@ static void _simon_reset(simon_state_t *state) { static void _simon_display_note(SimonNote note, simon_state_t *state) { - char *ndtemplate = NULL; - + watch_clear_display(); + if (note == SIMON_WRONG_NOTE) { + watch_display_text(WATCH_POSITION_TOP_LEFT, "OH"); + watch_display_text(WATCH_POSITION_BOTTOM, "NOOOOO"); + return; + } + sprintf(_simon_display_buf, "%2d", state->sequence_length); + watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf); switch (note) { case SIMON_LED_NOTE: - ndtemplate = "LI%2d "; + watch_display_text(WATCH_POSITION_TOP_LEFT, "LI"); break; case SIMON_ALARM_NOTE: - ndtemplate = " %2d AL"; + watch_display_text(WATCH_POSITION_SECONDS, "AL"); break; case SIMON_MODE_NOTE: - ndtemplate = " %2dDE "; + watch_display_text_with_fallback(WATCH_POSITION_HOURS, "Md", "DE"); + break; + default: break; - case SIMON_WRONG_NOTE: - ndtemplate = "OH NOOOOO"; } - - sprintf(_simon_display_buf, ndtemplate, state->sequence_length); - watch_display_string(_simon_display_buf, 0); } static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_rest) { @@ -115,30 +119,22 @@ static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_res switch (note) { case SIMON_LED_NOTE: if (!state->lightOff) watch_set_led_yellow(); - if (state->soundOff) - delay_ms(_delay_beep); - else - watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep); + if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep); + delay_ms(_delay_beep); break; case SIMON_MODE_NOTE: if (!state->lightOff) watch_set_led_red(); - if (state->soundOff) - delay_ms(_delay_beep); - else - watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep); + if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep); + delay_ms(_delay_beep); break; case SIMON_ALARM_NOTE: if (!state->lightOff) watch_set_led_green(); - if (state->soundOff) - delay_ms(_delay_beep); - else - watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep); + if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep); + delay_ms(_delay_beep); break; case SIMON_WRONG_NOTE: - if (state->soundOff) - delay_ms(800); - else - watch_buzzer_play_note(BUZZER_NOTE_A1, 800); + if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_A1, 800); + delay_ms(800); break; } watch_set_led_off(); @@ -146,7 +142,7 @@ static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_res if (note != SIMON_WRONG_NOTE) { _simon_clear_display(state); if (!skip_rest) { - watch_buzzer_play_note(BUZZER_NOTE_REST, (_delay_beep * 2)/3); + delay_ms((_delay_beep * 2)/3); } } } @@ -220,7 +216,6 @@ void simon_face_setup(uint8_t watch_face_index, } void simon_face_activate(void *context) { - (void) settings; (void) context; simon_state_t *state = (simon_state_t *)context; _simon_change_speed(state); diff --git a/legacy/watch_faces/complication/simon_face.h b/watch-faces/complication/simon_face.h similarity index 99% rename from legacy/watch_faces/complication/simon_face.h rename to watch-faces/complication/simon_face.h index 63e83895..a3dd743b 100644 --- a/legacy/watch_faces/complication/simon_face.h +++ b/watch-faces/complication/simon_face.h @@ -95,8 +95,8 @@ void simon_face_activate(void *context); bool simon_face_loop(movement_event_t event, void *context); void simon_face_resign(void *context); -#define simon_face \ - ((const watch_face_t){ \ +#define simon_face \ + ((const watch_face_t){ \ simon_face_setup, \ simon_face_activate, \ simon_face_loop, \ diff --git a/watch-faces/complication/simple_coin_flip_face.c b/watch-faces/complication/simple_coin_flip_face.c index 446832be..635c70ff 100644 --- a/watch-faces/complication/simple_coin_flip_face.c +++ b/watch-faces/complication/simple_coin_flip_face.c @@ -26,6 +26,7 @@ #include #include #include "simple_coin_flip_face.h" +#include "delay.h" void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) { (void) watch_face_index; @@ -36,7 +37,7 @@ void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) } void simple_coin_flip_face_activate(void *context) { - simple_coin_flip_face_state_t *state = (simple_coin_flip_face_state_t *)context; + (void) context; } static uint32_t get_random(uint32_t max) { @@ -48,7 +49,7 @@ static uint32_t get_random(uint32_t max) { } -void draw_start_face() { +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"); @@ -57,7 +58,7 @@ void draw_start_face() { } } -void set_pixels(int pixels[3][4][2], int j_len) { +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(); @@ -69,7 +70,7 @@ void set_pixels(int pixels[3][4][2], int j_len) { } } -void load_animation() { +static void load_animation(void) { if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { int j_len = 2; int pixels[3][4][2] = { @@ -114,6 +115,7 @@ void load_animation() { } static void _blink_face_update_lcd(simple_coin_flip_face_state_t *state) { + (void) state; watch_clear_display(); load_animation(); watch_clear_display(); diff --git a/watch-faces/complication/sunrise_sunset_face.c b/watch-faces/complication/sunrise_sunset_face.c index 7dcb3350..58514848 100644 --- a/watch-faces/complication/sunrise_sunset_face.c +++ b/watch-faces/complication/sunrise_sunset_face.c @@ -82,9 +82,8 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) { } watch_date_time_t date_time = movement_get_local_date_time(); // the current local date / time - watch_date_time_t utc_now = watch_utility_date_time_convert_zone(date_time, movement_get_current_timezone_offset(), 0); // the current date / time in UTC watch_date_time_t scratch_time; // scratchpad, contains different values at different times - scratch_time.reg = utc_now.reg; + scratch_time.reg = date_time.reg; // Weird quirky unsigned things were happening when I tried to cast these directly to doubles below. // it looks redundant, but extracting them to local int16's seemed to fix it. @@ -200,7 +199,7 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) { } // it's after sunset. we need to display sunrise/sunset for tomorrow. - uint32_t timestamp = watch_utility_date_time_to_unix_time(utc_now, 0); + uint32_t timestamp = watch_utility_date_time_to_unix_time(date_time, 0); timestamp += 86400; scratch_time = watch_utility_date_time_from_unix_time(timestamp, 0); } diff --git a/watch-faces/complication/tally_face.c b/watch-faces/complication/tally_face.c index deb5b878..ec217954 100644 --- a/watch-faces/complication/tally_face.c +++ b/watch-faces/complication/tally_face.c @@ -110,6 +110,12 @@ static bool tally_face_should_move_back(tally_state_t *state) { bool tally_face_loop(movement_event_t event, void *context) { tally_state_t *state = (tally_state_t *)context; static bool using_led = false; + static int8_t beep_sequence[] = { + 0, 2, + BUZZER_NOTE_REST, 3, + 0, 2, + 0 + }; if (using_led) { if(!HAL_GPIO_BTN_MODE_read() && !HAL_GPIO_BTN_LIGHT_read() && !HAL_GPIO_BTN_ALARM_read()) @@ -148,9 +154,11 @@ bool tally_face_loop(movement_event_t event, void *context) { state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index _init_val = true; //play a reset tune - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30); - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30); - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30); + if (movement_button_should_sound()) { + beep_sequence[0] = BUZZER_NOTE_G6; + beep_sequence[4] = BUZZER_NOTE_E6; + movement_play_sequence(beep_sequence, 0); + } print_tally(state, movement_button_should_sound()); } break; @@ -168,9 +176,11 @@ bool tally_face_loop(movement_event_t event, void *context) { if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){ state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE(); state->tally_idx = _tally_default[state->tally_default_idx]; - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30); - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30); - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30); + if (movement_button_should_sound()) { + beep_sequence[0] = BUZZER_NOTE_E6; + beep_sequence[4] = BUZZER_NOTE_G6; + movement_play_sequence(beep_sequence, 0); + } print_tally(state, movement_button_should_sound()); } else{ diff --git a/watch-faces/complication/tarot_face.c b/watch-faces/complication/tarot_face.c index c10b7710..ba50ed6d 100644 --- a/watch-faces/complication/tarot_face.c +++ b/watch-faces/complication/tarot_face.c @@ -280,6 +280,9 @@ void tarot_face_setup(uint8_t watch_face_index, void ** context_ptr) { if (*context_ptr == NULL) { *context_ptr = malloc(sizeof(tarot_state_t)); memset(*context_ptr, 0, sizeof(tarot_state_t)); + tarot_state_t *state = (tarot_state_t *)*context_ptr; + state->major_arcana_only = true; + state->num_cards_to_draw = 3; } // Emulator only: Seed random number generator #if __EMSCRIPTEN__ @@ -292,8 +295,6 @@ void tarot_face_activate(void *context) { watch_display_text_with_fallback(WATCH_POSITION_TOP, "Tarot", "TA"); init_deck(state); - state->num_cards_to_draw = 3; - state->major_arcana_only = true; } bool tarot_face_loop(movement_event_t event, void *context) { diff --git a/watch-faces/complication/tomato_face.c b/watch-faces/complication/tomato_face.c new file mode 100644 index 00000000..7c419d55 --- /dev/null +++ b/watch-faces/complication/tomato_face.c @@ -0,0 +1,186 @@ +/* + * MIT License + * + * Copyright (c) 2026 Wesley 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. + */ + +#include +#include +#include "tomato_face.h" +#include "watch.h" +#include "watch_utility.h" + +static const uint8_t focus_min = 25; +static const uint8_t break_min = 5; + +static uint8_t get_length(tomato_state_t *state) { + if (state->kind == tomato_focus) { + return focus_min; + } else { + return break_min; + } +} + +static void tomato_start(tomato_state_t *state) { + uint8_t length = get_length(state); + + state->mode = tomato_run; + state->now_ts = movement_get_utc_timestamp(); + state->target_ts = watch_utility_offset_timestamp(state->now_ts, 0, length, 0); + watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(state->target_ts, 0); + movement_schedule_background_task_for_face(state->watch_face_index, target_dt); + watch_set_indicator(WATCH_INDICATOR_BELL); +} + +static void tomato_draw(tomato_state_t *state) { + char buf[16]; + + uint32_t delta; + div_t result; + uint8_t min = 0; + uint8_t sec = 0; + char kind; + + if (state->kind == tomato_break) { + kind = 'b'; + } else { + kind = 'f'; + } + + switch (state->mode) { + case tomato_run: + if (state->target_ts <= state->now_ts) + delta = 0; + else + delta = state->target_ts - state->now_ts; + result = div(delta, 60); + min = result.quot; + sec = result.rem; + break; + case tomato_ready: + min = get_length(state); + sec = 0; + break; + } + + sprintf(buf, " %c", kind); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); + sprintf(buf, "%2d%02d%2d", min, sec, state->done_count); + watch_display_text(WATCH_POSITION_BOTTOM, buf); +} + +static void tomato_reset(tomato_state_t *state) { + state->mode = tomato_ready; + movement_cancel_background_task_for_face(state->watch_face_index); + watch_clear_indicator(WATCH_INDICATOR_BELL); +} + +static void tomato_ring(tomato_state_t *state) { + movement_play_signal(); + tomato_reset(state); + if (state->kind == tomato_focus) { + state->kind = tomato_break; + state->done_count++; + } else { + state->kind = tomato_focus; + } +} + +void tomato_face_setup(uint8_t watch_face_index, void ** context_ptr) { + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(tomato_state_t)); + tomato_state_t *state = (tomato_state_t *)*context_ptr; + memset(*context_ptr, 0, sizeof(tomato_state_t)); + state->mode = tomato_ready; + state->kind = tomato_focus; + state->done_count = 0; + state->watch_face_index = watch_face_index; + } +} + +void tomato_face_activate(void *context) { + tomato_state_t *state = (tomato_state_t *)context; + if (state->mode == tomato_run) { + state->now_ts = movement_get_utc_timestamp(); + watch_set_indicator(WATCH_INDICATOR_BELL); + } + watch_set_colon(); +} + +bool tomato_face_loop(movement_event_t event, void *context) { + tomato_state_t *state = (tomato_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + watch_display_text_with_fallback(WATCH_POSITION_TOP, "TOMATO", "TO"); + tomato_draw(state); + break; + case EVENT_TICK: + if (state->mode == tomato_run) { + state->now_ts++; + } + tomato_draw(state); + break; + case EVENT_LIGHT_BUTTON_DOWN: + movement_illuminate_led(); + if (state->mode == tomato_ready) { + if (state->kind == tomato_break) { + state->kind = tomato_focus; + } else { + state->kind = tomato_break; + } + } + tomato_draw(state); + break; + case EVENT_ALARM_BUTTON_UP: + switch(state->mode) { + case tomato_run: + tomato_reset(state); + break; + case tomato_ready: + tomato_start(state); + break; + } + tomato_draw(state); + break; + case EVENT_ALARM_LONG_PRESS: + state->done_count = 0; + break; + case EVENT_BACKGROUND_TASK: + tomato_ring(state); + tomato_draw(state); + break; + case EVENT_TIMEOUT: + if (state->mode != tomato_run) { + movement_move_to_face(0); + } + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +void tomato_face_resign(void *context) { + (void) context; +} diff --git a/watch-faces/complication/tomato_face.h b/watch-faces/complication/tomato_face.h new file mode 100644 index 00000000..913bd9eb --- /dev/null +++ b/watch-faces/complication/tomato_face.h @@ -0,0 +1,82 @@ +/* + * MIT License + * + * Copyright (c) 2026 Wesley 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 TOMATO_FACE_H_ +#define TOMATO_FACE_H_ + +/* + * TOMATO TIMER face + * + * Add a "tomato" timer watch face that alternates between 25 and 5 minute + * timers as in the Pomodoro Technique. + * https://en.wikipedia.org/wiki/Pomodoro_Technique + * + * The top right letter shows mode (f for focus or b for break). + * The bottom right shows how many focus sessions you've completed. + * (You can reset the count with a long press of alarm) + * + * When you show up and it says 25 minutes, you can start it (alarm), + * switch to 5 minute (light) mode or leave (mode). + * + * When it's running you can reset (alarm), or leave (mode). + * + * When it's done, we beep and go back to step 1, changing switching + * mode from focus to break (or break to focus) + */ + +#include "movement.h" + +typedef enum { + tomato_ready, + tomato_run, +} tomato_mode; + +typedef enum { + tomato_break, + tomato_focus, +} tomato_kind; + +typedef struct { + uint32_t target_ts; + uint32_t now_ts; + tomato_mode mode; + tomato_kind kind; + uint8_t done_count; + uint8_t watch_face_index; +} tomato_state_t; + +void tomato_face_setup(uint8_t watch_face_index, void ** context_ptr); +void tomato_face_activate(void *context); +bool tomato_face_loop(movement_event_t event, void *context); +void tomato_face_resign(void *context); + +#define tomato_face ((const watch_face_t){ \ + tomato_face_setup, \ + tomato_face_activate, \ + tomato_face_loop, \ + tomato_face_resign, \ + NULL, \ +}) + +#endif // TOMATO_FACE_H_ diff --git a/watch-faces/complication/totp_face.c b/watch-faces/complication/totp_face.c index b45c62f6..666c3337 100644 --- a/watch-faces/complication/totp_face.c +++ b/watch-faces/complication/totp_face.c @@ -36,7 +36,6 @@ #include #include "totp_face.h" #include "watch.h" -#include "watch_utility.h" #include "TOTP.h" #include "base32.h" @@ -159,7 +158,7 @@ static void totp_generate_and_display(totp_state_t *totp_state) { } static inline uint32_t totp_compute_base_timestamp() { - return watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0); + return movement_get_utc_timestamp(); } void totp_face_setup(uint8_t watch_face_index, void ** context_ptr) { diff --git a/watch-faces/complication/totp_lfs_face.c b/watch-faces/complication/totp_lfs_face.c index 8be3e28d..7d63bba4 100644 --- a/watch-faces/complication/totp_lfs_face.c +++ b/watch-faces/complication/totp_lfs_face.c @@ -30,7 +30,6 @@ #include "base32.h" #include "watch.h" -#include "watch_utility.h" #include "filesystem.h" #include "totp_lfs_face.h" @@ -253,7 +252,7 @@ void totp_lfs_face_activate(void *context) { } #endif - totp_state->timestamp = watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0); + totp_state->timestamp = movement_get_utc_timestamp(); totp_face_set_record(totp_state, 0); } diff --git a/watch-faces/complication/wareki_face.c b/watch-faces/complication/wareki_face.c index 103a503d..6dec4b31 100644 --- a/watch-faces/complication/wareki_face.c +++ b/watch-faces/complication/wareki_face.c @@ -1,17 +1,27 @@ /* - -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(昭和) -*/ + * MIT License + * + * Copyright (c) 2025 kbc-yam + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT + * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR + * THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ #include #include diff --git a/watch-faces/complication/wareki_face.h b/watch-faces/complication/wareki_face.h index 070642e8..dfa3e405 100644 --- a/watch-faces/complication/wareki_face.h +++ b/watch-faces/complication/wareki_face.h @@ -1,6 +1,47 @@ +/* + * MIT License + * + * Copyright (c) 2025 kbc-yam + * + * 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 WAREKI_FACE_H_ #define WAREKI_FACE_H_ +/* +Display Japanese era names (Wareki) + +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 "movement.h" #define REIWA_LIMIT 2018 + 99 diff --git a/watch-faces/complication/wordle_face.c b/watch-faces/complication/wordle_face.c index c3a2e69a..d63ef467 100644 --- a/watch-faces/complication/wordle_face.c +++ b/watch-faces/complication/wordle_face.c @@ -256,7 +256,7 @@ static void reset_board(wordle_state_t *state) { static void display_title(wordle_state_t *state) { state->curr_screen = WORDLE_SCREEN_TITLE; - watch_display_text(WATCH_POSITION_TOP_LEFT, "WO"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO"); watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); watch_display_text(WATCH_POSITION_BOTTOM, "WordLE"); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); @@ -286,7 +286,7 @@ static void display_streak(wordle_state_t *state) { #else sprintf(buf, "St%4d", state->streak); #endif - watch_display_text(WATCH_POSITION_TOP_LEFT, "WO"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO"); watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); watch_display_text(WATCH_POSITION_BOTTOM, buf); watch_set_colon(); @@ -304,7 +304,7 @@ static void display_wait(wordle_state_t *state) { else { // Streak too long to display in top-right watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); } - watch_display_text(WATCH_POSITION_TOP_LEFT, "WO"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO"); watch_display_text(WATCH_POSITION_BOTTOM, " WaIt "); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); } @@ -321,15 +321,17 @@ static uint32_t get_day_unix_time(void) { static void display_lose(wordle_state_t *state, uint8_t subsecond) { char buf[10]; sprintf(buf," %s", subsecond % 2 ? _valid_words[state->curr_answer] : " "); - watch_display_text(WATCH_POSITION_TOP, "L "); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "LOSE", "L "); watch_display_text(WATCH_POSITION_BOTTOM, buf); } static void display_win(wordle_state_t *state, uint8_t subsecond) { (void) state; char buf[10]; - sprintf(buf," %s ", subsecond % 2 ? "NICE" : "JOb "); - watch_display_text(WATCH_POSITION_TOP, "W "); + sprintf(buf," %s ", subsecond % 2 ? "NICE" : "JOb "); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "W "); watch_display_text(WATCH_POSITION_BOTTOM, buf); } @@ -644,4 +646,3 @@ bool wordle_face_loop(movement_event_t event, void *context) { void wordle_face_resign(void *context) { (void) context; } - diff --git a/watch-faces/demo/rtccount_face.c b/watch-faces/demo/rtccount_face.c new file mode 100644 index 00000000..6e7cb08c --- /dev/null +++ b/watch-faces/demo/rtccount_face.c @@ -0,0 +1,204 @@ +/* + * MIT License + * + * Copyright (c) 2025 Alessandro Genova + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include "rtccount_face.h" +#include "watch.h" +#include "sam.h" +#include "watch_utility.h" +#include "watch_common_display.h" +#include "watch_rtc.h" + +typedef enum { + RTCCOUNT_STATUS_COUNTER = 0, + RTCCOUNT_STATUS_COUNTER_SUB, + RTCCOUNT_STATUS_MINUTES, + RTCCOUNT_STATUS_MINUTES_DIFF, + RTCCOUNT_STATUS_NUMBER +} rtccount_face_status_t; + +typedef struct { + rtccount_face_status_t status; + uint8_t frequency; + uint32_t n_top_of_minute; + uint32_t ref_timestamp; +} rtccount_state_t; + +static const uint32_t COUNTER_MASK = (1 << 19) - 1; + +static void _rtccount_face_display_string(char* string, uint8_t pos) { + // watch_display_string is deprecated, but there is no alternative for this use-case + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + watch_display_string(string, pos); + #pragma GCC diagnostic pop +} + +static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) { + uint32_t counter = watch_rtc_get_counter(); + + char buf[11] = " 000000\0"; + switch (state->status) { + case RTCCOUNT_STATUS_COUNTER: { + buf[0] = 'C'; + break; + } + + case RTCCOUNT_STATUS_COUNTER_SUB: { + buf[0] = 'S'; + break; + } + + case RTCCOUNT_STATUS_MINUTES: { + buf[0] = 'M'; + break; + } + + case RTCCOUNT_STATUS_MINUTES_DIFF: { + buf[0] = 'D'; + break; + } + + default: + break; + } + + _rtccount_face_display_string(buf, 0); + + snprintf(buf, sizeof(buf), "%u", event.subsecond); + uint32_t len = strlen(buf); + _rtccount_face_display_string(buf, 4 - len); + + switch (state->status) { + case RTCCOUNT_STATUS_COUNTER: { + snprintf(buf, sizeof(buf), "%lu", counter & COUNTER_MASK); + + size_t len = strlen(buf); + + _rtccount_face_display_string(buf, 10 - len); + break; + } + + case RTCCOUNT_STATUS_COUNTER_SUB: { + snprintf(buf, sizeof(buf), "%lu", counter & 127); + + size_t len = strlen(buf); + + _rtccount_face_display_string(buf, 10 - len); + break; + } + + case RTCCOUNT_STATUS_MINUTES: { + snprintf(buf, sizeof(buf), "%lu", state->n_top_of_minute & COUNTER_MASK); + + size_t len = strlen(buf); + + _rtccount_face_display_string(buf, 10 - len); + break; + } + + case RTCCOUNT_STATUS_MINUTES_DIFF: { + uint32_t elapsed_minutes = (movement_get_utc_timestamp() - state->ref_timestamp) / 60; + + snprintf(buf, sizeof(buf), "%lu", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK); + + size_t len = strlen(buf); + + _rtccount_face_display_string(buf, 10 - len); + break; + } + + default: + break; + } +} + +void rtccount_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(rtccount_state_t)); + memset(*context_ptr, 0, sizeof(rtccount_state_t)); + rtccount_state_t *state = (rtccount_state_t *) *context_ptr; + state->status = RTCCOUNT_STATUS_COUNTER; + state->frequency = 1; + state->n_top_of_minute = 0; + rtc_date_time_t datetime = movement_get_utc_date_time(); + state->ref_timestamp = movement_get_utc_timestamp() - datetime.unit.second; + } +} + +void rtccount_face_activate(void *context) { + rtccount_state_t* state = (rtccount_state_t*)context; + movement_request_tick_frequency(state->frequency); +} + +bool rtccount_face_loop(movement_event_t event, void *context) { + rtccount_state_t* state = (rtccount_state_t*)context; + + switch (event.event_type) { + case EVENT_BACKGROUND_TASK: + state->n_top_of_minute += 1; + break; + case EVENT_ALARM_BUTTON_UP: + if (state->frequency == 128) { + state->frequency = 1; + } else { + state->frequency *= 2; + } + + movement_request_tick_frequency(state->frequency); + break; + case EVENT_ALARM_LONG_PRESS: + state->n_top_of_minute = 0; + rtc_date_time_t datetime = movement_get_utc_date_time(); + state->ref_timestamp = movement_get_utc_timestamp() - datetime.unit.second; + break; + case EVENT_LIGHT_BUTTON_DOWN: + state->status = (state->status + 1) % RTCCOUNT_STATUS_NUMBER; + _rtccount_face_draw(event, state); + break; + case EVENT_ACTIVATE: + case EVENT_TICK: + _rtccount_face_draw(event, state); + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +void rtccount_face_resign(void *context) { + (void) context; + movement_request_tick_frequency(1); +} + +movement_watch_face_advisory_t rtccount_face_advise(void *context) { + (void) context; + movement_watch_face_advisory_t retval = { 0 }; + retval.wants_background_task = true; + return retval; +} diff --git a/watch-faces/demo/rtccount_face.h b/watch-faces/demo/rtccount_face.h new file mode 100644 index 00000000..a3478a2d --- /dev/null +++ b/watch-faces/demo/rtccount_face.h @@ -0,0 +1,47 @@ +/* + * MIT License + * + * Copyright (c) 2025 Alessandro Genova + * + * 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 + +/* + * RTCCOUNT FACE + * + * A test face to inspect some metrics of the rtc-counter32 mode. + */ + +#include "movement.h" + +void rtccount_face_setup(uint8_t watch_face_index, void ** context_ptr); +void rtccount_face_activate(void *context); +bool rtccount_face_loop(movement_event_t event, void *context); +void rtccount_face_resign(void *context); +movement_watch_face_advisory_t rtccount_face_advise(void *context); + +#define rtccount_face ((const watch_face_t){ \ + rtccount_face_setup, \ + rtccount_face_activate, \ + rtccount_face_loop, \ + rtccount_face_resign, \ + rtccount_face_advise, \ +}) diff --git a/watch-faces/io/chirpy_demo_face.c b/watch-faces/io/chirpy_demo_face.c index 673da201..f676e179 100644 --- a/watch-faces/io/chirpy_demo_face.c +++ b/watch-faces/io/chirpy_demo_face.c @@ -47,9 +47,6 @@ typedef struct { // Selected program chirpy_demo_program_t program; - // Helps us handle 1/64 ticks during transmission; including countdown timer - chirpy_tick_state_t tick_state; - // Used by chirpy encoder during transmission chirpy_encoder_state_t encoder_state; @@ -150,46 +147,10 @@ static void _cdf_update_lcd(chirpy_demo_state_t *state) { } } -static void _cdf_quit_chirping(chirpy_demo_state_t *state) { - state->mode = CDM_CHOOSE; - watch_set_buzzer_off(); - watch_clear_indicator(WATCH_INDICATOR_BELL); - movement_request_tick_frequency(1); -} - -static void _cdf_scale_tick(void *context) { - chirpy_demo_state_t *state = (chirpy_demo_state_t *)context; - chirpy_tick_state_t *tick_state = &state->tick_state; - - // Scale goes in 200Hz increments from 700 Hz to 12.3 kHz -> 58 steps - if (tick_state->seq_pos == 58) { - _cdf_quit_chirping(state); - return; - } - uint32_t freq = 700 + tick_state->seq_pos * 200; - uint32_t period = 1000000 / freq; - watch_set_buzzer_period_and_duty_cycle(period, 25); - watch_set_buzzer_on(); - ++tick_state->seq_pos; -} - -static void _cdf_data_tick(void *context) { - chirpy_demo_state_t *state = (chirpy_demo_state_t *)context; - - uint8_t tone = chirpy_get_next_tone(&state->encoder_state); - // Transmission over? - if (tone == 255) { - _cdf_quit_chirping(state); - return; - } - uint16_t period = chirpy_get_tone_period(tone); - watch_set_buzzer_period_and_duty_cycle(period, 25); - watch_set_buzzer_on(); -} - static uint8_t *curr_data_ptr; static uint16_t curr_data_ix; static uint16_t curr_data_len; +static chirpy_demo_state_t *curr_state; static uint8_t _cdf_get_next_byte(uint8_t *next_byte) { if (curr_data_ix == curr_data_len) @@ -199,59 +160,60 @@ static uint8_t _cdf_get_next_byte(uint8_t *next_byte) { return 1; } -static void _cdf_countdown_tick(void *context) { - chirpy_demo_state_t *state = (chirpy_demo_state_t *)context; - chirpy_tick_state_t *tick_state = &state->tick_state; - - // Countdown over: start actual broadcast - if (tick_state->seq_pos == 8 * 3) { - tick_state->tick_compare = 3; - tick_state->tick_count = -1; - tick_state->seq_pos = 0; - // We'll be chirping out a scale - if (false) { // state->program == CDP_CLEAR) { - tick_state->tick_fun = _cdf_scale_tick; - } - // We'll be chirping out data - else { - // Set up the encoder - chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte); - tick_state->tick_fun = _cdf_data_tick; - // Set up the data - curr_data_ix = 0; - if (state->program == CDP_INFO_SHORT) { - curr_data_ptr = short_data; - curr_data_len = short_data_len; - } else if (state->program == CDP_INFO_LONG) { - curr_data_ptr = long_data_str; - curr_data_len = strlen((const char *)long_data_str); - } else if (state->program == CDP_INFO_NANOSEC) { - curr_data_ptr = activity_buffer; - curr_data_len = activity_buffer_size; - } - } - return; +static void _cdf_on_chirping_done(void) { + if (curr_state) { + curr_state->mode = CDM_CHOOSE; } - // Sound or turn off buzzer - if ((tick_state->seq_pos % 8) == 0) { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[BUZZER_NOTE_A5], 25); - watch_set_buzzer_on(); - } else if ((tick_state->seq_pos % 8) == 1) { - watch_set_buzzer_off(); - } - ++tick_state->seq_pos; + watch_clear_indicator(WATCH_INDICATOR_BELL); } -static void _cdm_setup_chirp(chirpy_demo_state_t *state) { - // We want frequent callbacks from now on - movement_request_tick_frequency(64); +static bool _cdm_raw_source_fn(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration) { + // Beep countdown + if (position < 6) { + if (position % 2) { + *period = WATCH_BUZZER_PERIOD_REST; + *duration = 56; + } else { + *period = NotePeriods[BUZZER_NOTE_A5]; + *duration = 8; + } + return false; + } + + chirpy_demo_state_t *state = (chirpy_demo_state_t *)userdata; + + uint8_t tone = chirpy_get_next_tone(&state->encoder_state); + // Transmission over? + if (tone == 255) { + return true; + } + + *period = chirpy_get_tone_period(tone); + *duration = 3; + + return false; +} + +static void _cdm_start_transmission(chirpy_demo_state_t *state) { watch_set_indicator(WATCH_INDICATOR_BELL); state->mode = CDM_CHIRPING; - // Set up tick state; start with countdown - state->tick_state.tick_count = -1; - state->tick_state.tick_compare = 8; - state->tick_state.seq_pos = 0; - state->tick_state.tick_fun = _cdf_countdown_tick; + + // Set up the data + curr_state = state; + curr_data_ix = 0; + if (state->program == CDP_INFO_SHORT) { + curr_data_ptr = short_data; + curr_data_len = short_data_len; + } else if (state->program == CDP_INFO_LONG) { + curr_data_ptr = long_data_str; + curr_data_len = strlen((const char *)long_data_str); + } else if (state->program == CDP_INFO_NANOSEC) { + curr_data_ptr = activity_buffer; + curr_data_len = activity_buffer_size; + } + + chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte); + watch_buzzer_play_raw_source(_cdm_raw_source_fn, state, _cdf_on_chirping_done); } bool chirpy_demo_face_loop(movement_event_t event, void *context) { @@ -261,12 +223,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { case EVENT_ACTIVATE: _cdf_update_lcd(state); break; - case EVENT_MODE_BUTTON_UP: - // Do not exit face while we're chirping - if (state->mode != CDM_CHIRPING) { - movement_move_to_next_face(); - } - break; + case EVENT_LIGHT_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_UP: // We don't do light. break; @@ -286,10 +243,6 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { state->program = CDP_CLEAR; _cdf_update_lcd(state); } - // If chirping: stoppit - else if (state->mode == CDM_CHIRPING) { - _cdf_quit_chirping(state); - } break; case EVENT_ALARM_LONG_PRESS: // If in choose mode: start chirping @@ -299,16 +252,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { movement_force_led_off(); movement_move_to_next_face(); } else { - _cdm_setup_chirp(state); - } - } - break; - case EVENT_TICK: - if (state->mode == CDM_CHIRPING) { - ++state->tick_state.tick_count; - if (state->tick_state.tick_count == state->tick_state.tick_compare) { - state->tick_state.tick_count = 0; - state->tick_state.tick_fun(context); + _cdm_start_transmission(state); } } break; @@ -317,15 +261,13 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { if (state->mode != CDM_CHIRPING) { movement_move_to_face(0); } + // fall through default: + movement_default_loop_handler(event); break; } - // Return true if the watch can enter standby mode. False needed when chirping. - if (state->mode == CDM_CHIRPING) - return false; - else - return true; + return true; } void chirpy_demo_face_resign(void *context) { diff --git a/watch-faces/sensor/activity_logging_face.c b/watch-faces/sensor/activity_logging_face.c index 3d5eb1cf..65d717f1 100644 --- a/watch-faces/sensor/activity_logging_face.c +++ b/watch-faces/sensor/activity_logging_face.c @@ -86,6 +86,13 @@ void activity_logging_face_activate(void *context) { bool activity_logging_face_loop(movement_event_t event, void *context) { activity_logging_state_t *state = (activity_logging_state_t *)context; switch (event.event_type) { + case EVENT_LIGHT_LONG_PRESS: + movement_illuminate_led(); + break; + case EVENT_LIGHT_BUTTON_DOWN: + state->display_index = (state->display_index + ACTIVITY_LOGGING_NUM_DAYS - 1) % ACTIVITY_LOGGING_NUM_DAYS; + _activity_logging_face_update_display(state); + break; case EVENT_ALARM_BUTTON_DOWN: state->display_index = (state->display_index + 1) % ACTIVITY_LOGGING_NUM_DAYS; // fall through diff --git a/watch-faces/sensor/activity_logging_face.h b/watch-faces/sensor/activity_logging_face.h index 6c5b74fb..f4f58b6e 100644 --- a/watch-faces/sensor/activity_logging_face.h +++ b/watch-faces/sensor/activity_logging_face.h @@ -40,6 +40,8 @@ * * A short press of the Alarm button moves backwards in the data log, showing yesterday's active minutes, * then the day before, etc. going back 14 days. + * A short press of the Light button moves forward in the data log, looping around if we're on the most-recent day. + * Holding the Light button will illuminate the display. * */ diff --git a/watch-faces/sensor/lis2dw_monitor_face.c b/watch-faces/sensor/lis2dw_monitor_face.c index a717fe46..8afb7d3e 100644 --- a/watch-faces/sensor/lis2dw_monitor_face.c +++ b/watch-faces/sensor/lis2dw_monitor_face.c @@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state) lis2dw_fifo_t fifo; float x = 0, y = 0, z = 0; - lis2dw_read_fifo(&fifo); + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT / DISPLAY_FREQUENCY); if (fifo.count == 0) { return; } diff --git a/watch-faces/settings/finetune_face.c b/watch-faces/settings/finetune_face.c index 7567f9fc..fe75d8fa 100644 --- a/watch-faces/settings/finetune_face.c +++ b/watch-faces/settings/finetune_face.c @@ -27,7 +27,6 @@ #include #include "finetune_face.h" #include "nanosec_face.h" -#include "watch_utility.h" #include "delay.h" extern nanosec_state_t nanosec_state; @@ -51,7 +50,7 @@ void finetune_face_activate(void *context) { } static float finetune_get_hours_passed(void) { - uint32_t current_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0); + uint32_t current_time = movement_get_utc_timestamp(); return (current_time - nanosec_state.last_correction_time) / 3600.0f; } @@ -64,7 +63,7 @@ static void finetune_update_display(void) { if (finetune_page == 0) { watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "FTU", "FT"); - watch_date_time_t date_time = watch_rtc_get_date_time(); + watch_date_time_t date_time = movement_get_utc_date_time(); sprintf(buf, "%04d%02d", abs(total_adjustment), date_time.unit.second); watch_display_text(WATCH_POSITION_BOTTOM, buf); @@ -106,17 +105,9 @@ static void finetune_adjust_subseconds(int delta) { watch_rtc_enable(false); delay_ms(delta); if (delta > 500) { - watch_date_time_t date_time = watch_rtc_get_date_time(); - date_time.unit.second = (date_time.unit.second + 1) % 60; - if (date_time.unit.second == 0) { // Overflow - date_time.unit.minute = (date_time.unit.minute + 1) % 60; - if (date_time.unit.minute == 0) { // Overflow - date_time.unit.hour = (date_time.unit.hour + 1) % 24; - if (date_time.unit.hour == 0) // Overflow - date_time.unit.day++; - } - } - watch_rtc_set_date_time(date_time); + uint32_t timestamp = movement_get_utc_timestamp(); + timestamp += 1; + movement_set_utc_timestamp(timestamp); } watch_rtc_enable(true); } @@ -126,7 +117,7 @@ static void finetune_update_correction_time(void) { nanosec_state.freq_correction += roundf(nanosec_get_aging() * 100); // Remember when we last corrected time - nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0); + nanosec_state.last_correction_time = movement_get_utc_timestamp(); nanosec_save(); movement_move_to_face(0); // Go to main face after saving settings } @@ -146,7 +137,7 @@ bool finetune_face_loop(movement_event_t event, void *context) { // We flash green LED once per minute to measure clock error, when we are not on first screen if (finetune_page!=0) { watch_date_time_t date_time; - date_time = watch_rtc_get_date_time(); + date_time = movement_get_utc_date_time(); if (date_time.unit.second == 0) { watch_set_led_green(); #ifndef __EMSCRIPTEN__ diff --git a/watch-faces/settings/finetune_face.h b/watch-faces/settings/finetune_face.h index 905df760..76ae7e23 100644 --- a/watch-faces/settings/finetune_face.h +++ b/watch-faces/settings/finetune_face.h @@ -44,11 +44,6 @@ * worry about aging only on second/third years of watch calibration (if you * are really looking at less than 10 seconds per year of error). * - * Warning, do not use at the first second of a month, as you might stay at - * the same month and it will surprise you. Just wait 1 second...We are not - * fully replicating RTC timer behavior when RTC is off. - * Simulating months and years is... too much complexity. - * * For full usage instructions, please refer to the wiki: * https://www.sensorwatch.net/docs/watchfaces/nanosec/ */ diff --git a/watch-faces/settings/nanosec_face.c b/watch-faces/settings/nanosec_face.c index d4d7b9f5..f49b22ad 100644 --- a/watch-faces/settings/nanosec_face.c +++ b/watch-faces/settings/nanosec_face.c @@ -27,7 +27,6 @@ #include #include "nanosec_face.h" #include "filesystem.h" -#include "watch_utility.h" int16_t freq_correction_residual = 0; // Dithering 0.1ppm correction, does not need to be configured. int16_t freq_correction_previous = -30000; @@ -44,8 +43,7 @@ const float voltage_coefficient = 0.241666667 * dithering; // 10 * ppm/V. Nomina static void nanosec_init_profile(void) { nanosec_changed = true; nanosec_state.correction_cadence = 10; - watch_date_time_t date_time = watch_rtc_get_date_time(); - nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(date_time, 0); + nanosec_state.last_correction_time = movement_get_utc_timestamp(); // init data after changing profile - do that once per profile selection switch (nanosec_state.correction_profile) { @@ -265,8 +263,8 @@ static void nanosec_next_edit_screen(void) { float nanosec_get_aging() // Returns aging correction in ppm { - watch_date_time_t date_time = watch_rtc_get_date_time(); - float years = (watch_utility_date_time_to_unix_time(date_time, 0) - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune + uint32_t timestamp = movement_get_utc_timestamp(); + float years = (timestamp - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune return years*nanosec_state.aging_ppm_pa/100.0f; } @@ -377,7 +375,7 @@ movement_watch_face_advisory_t nanosec_face_advise(void *context) { // No need for background correction if we are on profile 0 - static hardware correction. if (nanosec_state.correction_profile != 0) { - watch_date_time_t date_time = watch_rtc_get_date_time(); + watch_date_time_t date_time = movement_get_utc_date_time(); retval.wants_background_task = date_time.unit.minute % nanosec_state.correction_cadence == 0; } diff --git a/watch-faces/settings/set_time_face.c b/watch-faces/settings/set_time_face.c index 63fc34fd..f52c7179 100644 --- a/watch-faces/settings/set_time_face.c +++ b/watch-faces/settings/set_time_face.c @@ -46,7 +46,7 @@ static void _handle_alarm_button(watch_date_time_t date_time, uint8_t current_pa current_offset = movement_get_current_timezone_offset_for_zone(movement_get_timezone_index()); 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); @@ -106,10 +108,6 @@ bool set_time_face_loop(movement_event_t event, void *context) { case EVENT_ALARM_LONG_UP: _abort_quick_ticks(); break; - case EVENT_MODE_BUTTON_UP: - _abort_quick_ticks(); - movement_move_to_next_face(); - return false; case EVENT_LIGHT_BUTTON_DOWN: current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS; *((uint8_t *)context) = current_page; @@ -185,6 +183,6 @@ bool set_time_face_loop(movement_event_t event, void *context) { void set_time_face_resign(void *context) { (void) context; - watch_set_led_off(); movement_store_settings(); + movement_request_tick_frequency(1); } diff --git a/watch-faces/settings/settings_face.c b/watch-faces/settings/settings_face.c index 0894ebe5..73bdadc1 100644 --- a/watch-faces/settings/settings_face.c +++ b/watch-faces/settings/settings_face.c @@ -77,6 +77,64 @@ static void beep_setting_advance(void) { } } +static void signal_setting_display(uint8_t subsecond) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SIG", "SI"); + watch_display_text(WATCH_POSITION_BOTTOM, "SIGNAL"); + if (subsecond % 2) { + if (movement_signal_volume() == WATCH_BUZZER_VOLUME_LOUD) { + // H for HIGH + watch_display_text(WATCH_POSITION_TOP_RIGHT, " H"); + } + else { + // L for LOW + watch_display_text(WATCH_POSITION_TOP_RIGHT, " L"); + } + } +} + +static void signal_setting_advance(void) { + if (movement_signal_volume() == WATCH_BUZZER_VOLUME_SOFT) { + // was soft. make it loud. + movement_set_signal_volume(WATCH_BUZZER_VOLUME_LOUD); + } else { + // was loud. make it soft. + movement_set_signal_volume(WATCH_BUZZER_VOLUME_SOFT); + } + + signal_setting_display(1); + movement_play_signal(); +} + + +static void alarm_setting_display(uint8_t subsecond) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "ALM", "AL"); + watch_display_text(WATCH_POSITION_BOTTOM, "ALARM "); + if (subsecond % 2) { + if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_LOUD) { + // H for HIGH + watch_display_text(WATCH_POSITION_TOP_RIGHT, " H"); + } + else { + // L for LOW + watch_display_text(WATCH_POSITION_TOP_RIGHT, " L"); + } + } +} + +static void alarm_setting_advance(void) { + if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_SOFT) { + // was soft. make it loud. + movement_set_alarm_volume(WATCH_BUZZER_VOLUME_LOUD); + } else { + // was loud. make it soft. + movement_set_alarm_volume(WATCH_BUZZER_VOLUME_SOFT); + + } + + alarm_setting_display(1); + movement_play_alarm(); +} + static void timeout_setting_display(uint8_t subsecond) { watch_display_text_with_fallback(WATCH_POSITION_TOP, "TMOUt", "TO"); if (subsecond % 2) { @@ -235,7 +293,7 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) { settings_state_t *state = (settings_state_t *)*context_ptr; int8_t current_setting = 0; - state->num_settings = 5; // baseline, without LED settings + state->num_settings = 7; // baseline, without LED settings #ifdef BUILD_GIT_HASH state->num_settings++; #endif @@ -256,6 +314,12 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) { state->settings_screens[current_setting].display = beep_setting_display; state->settings_screens[current_setting].advance = beep_setting_advance; current_setting++; + state->settings_screens[current_setting].display = signal_setting_display; + state->settings_screens[current_setting].advance = signal_setting_advance; + current_setting++; + state->settings_screens[current_setting].display = alarm_setting_display; + state->settings_screens[current_setting].advance = alarm_setting_advance; + current_setting++; state->settings_screens[current_setting].display = timeout_setting_display; state->settings_screens[current_setting].advance = timeout_setting_advance; current_setting++; @@ -322,7 +386,7 @@ bool settings_face_loop(movement_event_t event, void *context) { case EVENT_MODE_BUTTON_UP: movement_force_led_off(); movement_move_to_next_face(); - return false; + return true; case EVENT_ALARM_BUTTON_UP: state->settings_screens[state->current_page].advance(); break; @@ -339,7 +403,7 @@ bool settings_face_loop(movement_event_t event, void *context) { movement_force_led_on(color.red | color.red << 4, color.green | color.green << 4, color.blue | color.blue << 4); - return false; + return true; } else { movement_force_led_off(); return true; diff --git a/watch-faces/settings/settings_face.h b/watch-faces/settings/settings_face.h index 49e96b98..b38e3d1a 100644 --- a/watch-faces/settings/settings_face.h +++ b/watch-faces/settings/settings_face.h @@ -44,6 +44,12 @@ * This setting allows you to choose whether the Mode button should emit * a beep when pressed, and if so, how loud it should be. Options are * "Y" for yes and "N" for no. + * + * SI / SIG - Signal beep. + * This setting allows you to choose the hourly chime buzzer volume. + * + * AL / ALM - Alarm beep. + * This setting allows you to choose the alarm buzzer volume. * * TO / Tmout - Timeout. * Sets the time until screens that time out (like Settings and Time Set) diff --git a/watch-library/hardware/watch/rtc32.c b/watch-library/hardware/watch/rtc32.c new file mode 100644 index 00000000..d4ac9b27 --- /dev/null +++ b/watch-library/hardware/watch/rtc32.c @@ -0,0 +1,136 @@ +/* + * MIT License + * + * Copyright (c) 2022 Joey Castillo + * Copyright (c) 2025 Alessandro Genova + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include "rtc32.h" +#include "sam.h" + +rtc_cb_t _rtc_callback = NULL; + +#if defined(_SAMD21_) || defined(_SAMD11_) +#define CTRLREG (RTC->MODE0.CTRL) +#define MODE_SETTING (RTC_MODE0_CTRL_MODE_COUNT32_Val) // Mode 0 Count32 +#define PRESCALER_SETTING (RTC_MODE0_CTRL_PRESCALER_DIV8_Val) +#else +#define CTRLREG (RTC->MODE0.CTRLA) +#define MODE_SETTING (RTC_MODE0_CTRLA_MODE_COUNT32_Val) // Mode 0 Count32 +#define PRESCALER_SETTING (RTC_MODE0_CTRLA_PRESCALER_DIV8_Val) +#endif + +bool rtc_is_enabled(void) { + return CTRLREG.bit.ENABLE; +} + +static void _rtc_sync(void) { +#if defined(_SAMD21_) || defined(_SAMD11_) + while (RTC->MODE0.STATUS.bit.SYNCBUSY); +#else + while (RTC->MODE0.SYNCBUSY.reg & RTC_MODE0_SYNCBUSY_MASK); +#endif +} + +void rtc_init(void) { +#if defined(_SAMD21_) || defined(_SAMD11_) + // enable the RTC + PM->APBAMASK.reg |= PM_APBAMASK_RTC; + // clock RTC with GCLK3 (prescaled 1024 Hz output from the external crystal) + GCLK->CLKCTRL.reg = GCLK_CLKCTRL_GEN(3) | GCLK_CLKCTRL_ID(RTC_GCLK_ID) | GCLK_CLKCTRL_CLKEN; +#else + MCLK->APBAMASK.reg |= MCLK_APBAMASK_RTC; +#endif + + if (rtc_is_enabled()) return; // don't reset the RTC if it's already set up. + + _rtc_sync(); + CTRLREG.bit.SWRST = 1; + _rtc_sync(); + + CTRLREG.bit.MODE = MODE_SETTING; + CTRLREG.bit.PRESCALER = PRESCALER_SETTING; + +#if defined(_SAML21_) || defined(_SAML22_) || defined(_SAMD51_) + CTRLREG.bit.COUNTSYNC = 1; +#endif + + RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_OVF; +} + +void rtc_enable(void) { + if (rtc_is_enabled()) return; + CTRLREG.bit.ENABLE = 1; + _rtc_sync(); +} + +void rtc_set_counter(rtc_counter_t counter) { + // // syncing before and after was found to increase reliability on Sensor Watch + _rtc_sync(); + RTC->MODE0.COUNT.reg = counter; + _rtc_sync(); +} + +rtc_counter_t rtc_get_counter(void) { + rtc_counter_t counter; + +#if defined(_SAML21_) || defined(_SAML22_) || defined(_SAMD51_) + CTRLREG.bit.COUNTSYNC = 1; +#endif + _rtc_sync(); + counter = RTC->MODE0.COUNT.reg; + + return counter; +} + +void rtc_enable_compare_interrupt(uint32_t compare_time) { + RTC->MODE0.COMP[0].reg = compare_time; + _rtc_sync(); + RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_CMP0; + // NVIC_ClearPendingIRQ(RTC_IRQn); + // NVIC_EnableIRQ(RTC_IRQn); +} + +void rtc_configure_callback(rtc_cb_t callback) { + _rtc_callback = callback; +} + +void rtc_disable_compare_interrupt(void){ + RTC->MODE0.INTENCLR.reg = RTC_MODE0_INTENCLR_CMP0; + // NVIC_ClearPendingIRQ(RTC_IRQn); + // NVIC_DisableIRQ(RTC_IRQn); +} + +void irq_handler_rtc(void); + +void irq_handler_rtc(void) { + uint16_t int_cause = (uint16_t)RTC->MODE0.INTFLAG.reg; + RTC->MODE0.INTFLAG.reg = RTC_MODE0_INTFLAG_MASK; + (void)RTC->MODE0.INTFLAG.reg; + + /* Invoke registered Callback function */ + if (_rtc_callback != NULL) { + _rtc_callback(int_cause); + } + + // NVIC_ClearPendingIRQ(RTC_IRQn); +} diff --git a/watch-library/hardware/watch/watch_deepsleep.c b/watch-library/hardware/watch/watch_deepsleep.c index a0abe5eb..20254df4 100644 --- a/watch-library/hardware/watch/watch_deepsleep.c +++ b/watch-library/hardware/watch/watch_deepsleep.c @@ -41,7 +41,7 @@ void sleep(const uint8_t mode) { } void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool level) { - uint32_t config = RTC->MODE2.TAMPCTRL.reg; + uint32_t config = RTC->MODE0.TAMPCTRL.reg; if (pin == HAL_GPIO_BTN_ALARM_pin()) { HAL_GPIO_BTN_ALARM_in(); @@ -71,22 +71,22 @@ void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool leve } // disable the RTC - RTC->MODE2.CTRLA.bit.ENABLE = 0; - while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled + RTC->MODE0.CTRLA.bit.ENABLE = 0; + while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled // update the configuration - RTC->MODE2.TAMPCTRL.reg = config; + RTC->MODE0.TAMPCTRL.reg = config; // re-enable the RTC - RTC->MODE2.CTRLA.bit.ENABLE = 1; + RTC->MODE0.CTRLA.bit.ENABLE = 1; NVIC_ClearPendingIRQ(RTC_IRQn); NVIC_EnableIRQ(RTC_IRQn); - RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_TAMPER; + RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_TAMPER; } void watch_disable_extwake_interrupt(uint8_t pin) { - uint32_t config = RTC->MODE2.TAMPCTRL.reg; + uint32_t config = RTC->MODE0.TAMPCTRL.reg; if (pin == HAL_GPIO_BTN_ALARM_pin()) { btn_alarm_callback = NULL; @@ -101,14 +101,14 @@ void watch_disable_extwake_interrupt(uint8_t pin) { } // disable the RTC - RTC->MODE2.CTRLA.bit.ENABLE = 0; - while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled + RTC->MODE0.CTRLA.bit.ENABLE = 0; + while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled // update the configuration - RTC->MODE2.TAMPCTRL.reg = config; + RTC->MODE0.TAMPCTRL.reg = config; // re-enable the RTC - RTC->MODE2.CTRLA.bit.ENABLE = 1; + RTC->MODE0.CTRLA.bit.ENABLE = 1; } void watch_store_backup_data(uint32_t data, uint8_t reg) { @@ -151,7 +151,8 @@ static void _watch_disable_all_pins_except_rtc(void) { } static void _watch_disable_all_peripherals_except_slcd(void) { - _watch_disable_tcc(); + watch_disable_leds(); + watch_disable_buzzer(); watch_disable_adc(); watch_disable_external_interrupts(); diff --git a/watch-library/hardware/watch/watch_i2c.c b/watch-library/hardware/watch/watch_i2c.c index 709c63b7..1a89f80c 100644 --- a/watch-library/hardware/watch/watch_i2c.c +++ b/watch-library/hardware/watch/watch_i2c.c @@ -38,27 +38,31 @@ void watch_disable_i2c(void) { i2c_disable(); } -void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { - i2c_write(addr, buf, length); +int8_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { + return (int8_t)i2c_write(addr, buf, length); } -void watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { - i2c_read(addr, buf, length); +int8_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { + return (int8_t)i2c_read(addr, buf, length); } -void watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { +int8_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { uint8_t buf[2]; buf[0] = reg; buf[1] = data; - watch_i2c_send(addr, (uint8_t *)&buf, 2); + return (int8_t)watch_i2c_send(addr, (uint8_t *)&buf, 2); } uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) { uint8_t data; - watch_i2c_send(addr, (uint8_t *)®, 1); - watch_i2c_receive(addr, (uint8_t *)&data, 1); + if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) { + return 0; + } + if (watch_i2c_receive(addr, (uint8_t *)&data, 1) != 0) { + return 0; + } return data; } @@ -66,9 +70,12 @@ uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) { uint16_t watch_i2c_read16(int16_t addr, uint8_t reg) { uint16_t data; - watch_i2c_send(addr, (uint8_t *)®, 1); - watch_i2c_receive(addr, (uint8_t *)&data, 2); - + if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) { + return 0; + } + if (watch_i2c_receive(addr, (uint8_t *)&data, 2) != 0) { + return 0; + } return data; } @@ -76,18 +83,24 @@ uint32_t watch_i2c_read24(int16_t addr, uint8_t reg) { uint32_t data; data = 0; - watch_i2c_send(addr, (uint8_t *)®, 1); - watch_i2c_receive(addr, (uint8_t *)&data, 3); - + if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) { + return 0; + } + if (watch_i2c_receive(addr, (uint8_t *)&data, 3) != 0) { + return 0; + } return data << 8; } uint32_t watch_i2c_read32(int16_t addr, uint8_t reg) { uint32_t data; - watch_i2c_send(addr, (uint8_t *)®, 1); - watch_i2c_receive(addr, (uint8_t *)&data, 4); - + if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) { + return 0; + } + if (watch_i2c_receive(addr, (uint8_t *)&data, 4) != 0) { + return 0; + } return data; } diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index babae30d..7cfa81c2 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -2,6 +2,7 @@ * MIT License * * Copyright (c) 2020 Joey Castillo + * Copyright (c) 2025 Alessandro Genova * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,11 +24,36 @@ */ #include +#include #include "watch_rtc.h" #include "watch_private.h" +#include "watch_utility.h" + +static const uint32_t RTC_OSC_DIV = 10; +static const uint32_t RTC_OSC_HZ = 1 << RTC_OSC_DIV; // 2^10 = 1024 +static const uint32_t RTC_PRESCALER_DIV = 3; +static const uint32_t RTC_CNT_HZ = RTC_OSC_HZ >> RTC_PRESCALER_DIV; // 1024 / 2^3 = 128 +static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1; +static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7 +static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; + +static const uint32_t RTC_COMP_GRACE_PERIOD = 4; + +static const int TB_BKUP_REG = 7; + +#define WATCH_RTC_N_COMP_CB 8 + +typedef struct { + volatile uint32_t counter; + volatile watch_cb_t callback; + volatile bool enabled; +} comp_cb_t; + +volatile uint32_t scheduled_comp_counter; watch_cb_t tick_callbacks[8]; +comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB]; watch_cb_t alarm_callback; watch_cb_t btn_alarm_callback; watch_cb_t a2_callback; @@ -46,14 +72,77 @@ void _watch_rtc_init(void) { #endif rtc_enable(); rtc_configure_callback(watch_rtc_callback); + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + comp_callbacks[index].counter = 0; + comp_callbacks[index].callback = NULL; + comp_callbacks[index].enabled = false; + } + + scheduled_comp_counter = 0; + + NVIC_ClearPendingIRQ(RTC_IRQn); + NVIC_EnableIRQ(RTC_IRQn); } void watch_rtc_set_date_time(rtc_date_time_t date_time) { - rtc_set_date_time(date_time); + watch_rtc_set_unix_time(watch_utility_date_time_to_unix_time(date_time, 0)); } rtc_date_time_t watch_rtc_get_date_time(void) { - return rtc_get_date_time(); + static struct { + unix_timestamp_t timestamp; + rtc_date_time_t datetime; + } cached_date_time = {.datetime.reg=0, .timestamp=0}; + + unix_timestamp_t timestamp = watch_rtc_get_unix_time(); + + if (timestamp != cached_date_time.timestamp) { + cached_date_time.timestamp = timestamp; + cached_date_time.datetime = watch_utility_date_time_from_unix_time(timestamp, 0); + } + + return cached_date_time.datetime; +} + +void watch_rtc_set_unix_time(unix_timestamp_t unix_time) { + /* unix_time = time_backup + counter / RTC_CNT_HZ - 0.5 + * + * Because of the way the hardware is designed, the periodic interrupts fire at the subsecond tick values + * according to the table below (for a 128Hz counter). + * since the 1Hz periodic interrupt is the most important, we shift the conversion from counter to timestamp by 64 ticks, + * so that the second changes at the top of the 1Hz interrupt. Hence the 0.5 factor in the equation above. + * 1Hz: 64 + * 2Hz: 32, 96 + * 4Hz: 16, 48, 80, 112 + * 8Hz: 8, 24, 40, 56, 72, 88, 104, 120 + * 16Hz: 4, 12, 20, ..., 124 + * 32Hz: 2, 6, 10, ..., 126 + * 64Hz: 1, 3, 5, ..., 127 + * 128Hz: 0, 1, 2, ..., 127 + */ + rtc_counter_t counter = rtc_get_counter(); + unix_timestamp_t tb = unix_time - (counter >> RTC_CNT_DIV) - ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) + 1; + watch_store_backup_data(tb, TB_BKUP_REG); +} + +unix_timestamp_t watch_rtc_get_unix_time(void) { + // unix_time = time_backup + counter / RTC_CNT_HZ - 0.5 + rtc_counter_t counter = rtc_get_counter(); + unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG); + return tb + (counter >> RTC_CNT_DIV) + ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) - 1; +} + +rtc_counter_t watch_rtc_get_counter(void) { + return rtc_get_counter(); +} + +uint32_t watch_rtc_get_frequency(void) { + return RTC_CNT_HZ; +} + +uint32_t watch_rtc_get_ticks_per_minute(void) { + return RTC_CNT_TICKS_PER_MINUTE; } rtc_date_time_t watch_get_init_date_time(void) { @@ -103,57 +192,123 @@ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency // this also maps nicely to an index for our list of tick callbacks. tick_callbacks[per_n] = callback; - NVIC_ClearPendingIRQ(RTC_IRQn); - NVIC_EnableIRQ(RTC_IRQn); - RTC->MODE2.INTENSET.reg = 1 << per_n; + // NVIC_ClearPendingIRQ(RTC_IRQn); + // NVIC_EnableIRQ(RTC_IRQn); + RTC->MODE0.INTENSET.reg = 1 << per_n; } void watch_rtc_disable_periodic_callback(uint8_t frequency) { if (__builtin_popcount(frequency) != 1) return; uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24); - RTC->MODE2.INTENCLR.reg = 1 << per_n; + RTC->MODE0.INTENCLR.reg = 1 << per_n; } void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) { - RTC->MODE2.INTENCLR.reg = mask; + RTC->MODE0.INTENCLR.reg = mask; } void watch_rtc_disable_all_periodic_callbacks(void) { watch_rtc_disable_matching_periodic_callbacks(0xFF); } -void watch_rtc_register_alarm_callback(watch_cb_t callback, rtc_date_time_t alarm_time, rtc_alarm_match_t mask) { - RTC->MODE2.Mode2Alarm[0].ALARM.reg = alarm_time.reg; - RTC->MODE2.Mode2Alarm[0].MASK.reg = mask; - RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0; - alarm_callback = callback; - NVIC_ClearPendingIRQ(RTC_IRQn); - NVIC_EnableIRQ(RTC_IRQn); - RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0; +void watch_rtc_schedule_next_comp(void) { + rtc_counter_t curr_counter = watch_rtc_get_counter(); + + // We want to ensure we never miss any registered callbacks, + // so if a callback counter has just passed but didn't fire, give it a chance to fire. + rtc_counter_t lax_curr_counter = curr_counter - RTC_COMP_GRACE_PERIOD; + + bool schedule_any = false; + rtc_counter_t comp_counter; + rtc_counter_t min_diff = UINT_MAX; + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + if (comp_callbacks[index].enabled) { + rtc_counter_t diff = comp_callbacks[index].counter - lax_curr_counter; + if (diff <= min_diff) { + min_diff = diff; + comp_counter = comp_callbacks[index].counter; + schedule_any = true; + } + } + } + + if (schedule_any) { + // If we are changing the comp counter at the front of the line, don't schedule a comp interrupt for a counter that is too close to now + if (comp_counter != scheduled_comp_counter) { + rtc_counter_t earliest_comp_counter = curr_counter + RTC_COMP_GRACE_PERIOD; + if ((earliest_comp_counter - lax_curr_counter) > (comp_counter - lax_curr_counter)) { + comp_counter = earliest_comp_counter; + } + scheduled_comp_counter = comp_counter; + rtc_enable_compare_interrupt(comp_counter); + } + } else { + scheduled_comp_counter = lax_curr_counter - RTC_COMP_GRACE_PERIOD; + rtc_disable_compare_interrupt(); + } } -void watch_rtc_disable_alarm_callback(void) { - RTC->MODE2.INTENCLR.reg = RTC_MODE2_INTENCLR_ALARM0; +void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].counter = counter; + comp_callbacks[index].callback = callback; + comp_callbacks[index].enabled = true; + + watch_rtc_schedule_next_comp(); } -void watch_rtc_callback(uint16_t interrupt_status) { - uint16_t interrupt_enabled = RTC->MODE2.INTENSET.reg; +void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } - if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_PER_Msk) { + comp_callbacks[index].counter = counter; + comp_callbacks[index].callback = callback; + comp_callbacks[index].enabled = true; +} + +void watch_rtc_disable_comp_callback(uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].enabled = false; + + watch_rtc_schedule_next_comp(); +} + +void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].enabled = false; +} + +void watch_rtc_callback(uint16_t interrupt_cause) { + // First read all relevant registers, to ensure no changes occurr during the callbacks + rtc_counter_t curr_counter = watch_rtc_get_counter(); + uint16_t interrupt_enabled = (uint16_t)RTC->MODE0.INTENSET.reg; + + if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) { // handle the tick callback first, it's what we do the most. // start from PER7, the 1 Hz tick. for(int8_t i = 7; i >= 0; i--) { - if ((interrupt_status & interrupt_enabled) & (1 << i)) { + if ((interrupt_cause & interrupt_enabled) & (1 << i)) { if (tick_callbacks[i] != NULL) { tick_callbacks[i](); } - RTC->MODE2.INTFLAG.reg = 1 << i; -// break; Uncertain if this fix is requried. We were discussing in discord. Might slightly increase power consumption. } } - } else if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_TAMPER) { + } + + if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_TAMPER) { // handle the extwake interrupts next. - uint8_t reason = RTC->MODE2.TAMPID.reg; + uint8_t reason = RTC->MODE0.TAMPID.reg; if (reason & RTC_TAMPID_TAMPID2) { if (btn_alarm_callback != NULL) btn_alarm_callback(); } else if (reason & RTC_TAMPID_TAMPID1) { @@ -161,25 +316,36 @@ void watch_rtc_callback(uint16_t interrupt_status) { } else if (reason & RTC_TAMPID_TAMPID0) { if (a4_callback != NULL) a4_callback(); } - RTC->MODE2.TAMPID.reg = reason; - RTC->MODE2.INTFLAG.reg = RTC_MODE2_INTFLAG_TAMPER; - } else if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_ALARM0) { - // finally handle the alarm. - if (alarm_callback != NULL) { - alarm_callback(); + RTC->MODE0.TAMPID.reg = reason; + } + + if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) { + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + if (comp_callbacks[index].enabled && + (curr_counter - comp_callbacks[index].counter) < (RTC_COMP_GRACE_PERIOD * 4) + ) { + comp_callbacks[index].enabled = false; + comp_callbacks[index].callback(); + } } - RTC->MODE2.INTFLAG.reg = RTC_MODE2_INTFLAG_ALARM0; + watch_rtc_schedule_next_comp(); + } + + if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_OVF) { + // Handle the overflow of the counter. All we need to do is reset the reference time. + unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG); + watch_store_backup_data(tb + (UINT_MAX >> RTC_CNT_DIV), TB_BKUP_REG); } } void watch_rtc_enable(bool en) { // Writing it twice - as it's quite dangerous operation. // If write fails - we might hang with RTC off, which means no recovery possible - while (RTC->MODE2.SYNCBUSY.reg); - RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0; - while (RTC->MODE2.SYNCBUSY.reg); - RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0; - while (RTC->MODE2.SYNCBUSY.reg); + while (RTC->MODE0.SYNCBUSY.reg); + RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0; + while (RTC->MODE0.SYNCBUSY.reg); + RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0; + while (RTC->MODE0.SYNCBUSY.reg); } void watch_rtc_freqcorr_write(int16_t value, int16_t sign) { @@ -188,8 +354,7 @@ void watch_rtc_freqcorr_write(int16_t value, int16_t sign) { data.bit.VALUE = value; data.bit.SIGN = sign; - RTC->MODE2.FREQCORR.reg = data.reg; // Setting correction in single write operation + RTC->MODE0.FREQCORR.reg = data.reg; // Setting correction in single write operation // We do not sycnronize. We are not in a hurry } - diff --git a/watch-library/hardware/watch/watch_slcd.c b/watch-library/hardware/watch/watch_slcd.c index db6b1942..337dd848 100644 --- a/watch-library/hardware/watch/watch_slcd.c +++ b/watch-library/hardware/watch/watch_slcd.c @@ -252,7 +252,7 @@ void watch_enable_display(void) { slcd_clear(); if (_installed_display == WATCH_LCD_TYPE_CUSTOM) { - slcd_set_contrast(4); + slcd_set_contrast(0); } else { slcd_set_contrast(9); } @@ -260,6 +260,10 @@ void watch_enable_display(void) { slcd_enable(); } +void watch_disable_display(void) { + slcd_disable(); +} + inline void watch_set_pixel(uint8_t com, uint8_t seg) { slcd_set_segment(com, seg); } diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index b5d50f94..07f3126c 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -27,14 +27,30 @@ #include "tcc.h" #include "tc.h" -void _watch_enable_tcc(void); -void cb_watch_buzzer_seq(void); +static void _watch_enable_tcc(void); +static void _watch_disable_tcc(void); +static void _watch_maybe_enable_tcc(void); +static void _watch_maybe_disable_tcc(void); +static void _watch_enable_led_pins(void); +static void _watch_disable_led_pins(void); +static void (*_cb_tc0)(void) = NULL; +static void cb_watch_buzzer_seq(void); +static void cb_watch_buzzer_raw_source(void); static uint16_t _seq_position; static int8_t _tone_ticks, _repeat_counter; -static bool _callback_running = false; static int8_t *_sequence; +static watch_buzzer_raw_source_t _raw_source; +static void* _userdata; +static uint8_t _volume; static void (*_cb_finished)(void); +static watch_cb_t _cb_start_global = NULL; +static watch_cb_t _cb_stop_global = NULL; +static volatile bool _led_is_active = false; +static volatile bool _buzzer_is_active = false; +static volatile uint8_t _current_led_color[3] = {0, 0, 0}; + +static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue); static void _tcc_write_RUNSTDBY(bool value) { // enables or disables RUNSTDBY of the tcc @@ -46,13 +62,11 @@ static void _tcc_write_RUNSTDBY(bool value) { static inline void _tc0_start() { // start the TC0 timer tc_enable(0); - _callback_running = true; } static inline void _tc0_stop() { // stop the TC0 timer tc_disable(0); - _callback_running = false; } static void _tc0_initialize() { @@ -68,19 +82,30 @@ static void _tc0_initialize() { } void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) { - if (_callback_running) _tc0_stop(); + watch_buzzer_play_sequence_with_volume(note_sequence, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); +} + +void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) { + // Abort any previous sequence + watch_buzzer_abort_sequence(); + + if (_cb_start_global) { + _cb_start_global(); + } + + watch_enable_buzzer(); watch_set_buzzer_off(); _sequence = note_sequence; _cb_finished = callback_on_end; + _volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25; _seq_position = 0; _tone_ticks = 0; _repeat_counter = -1; // prepare buzzer - watch_enable_buzzer(); + + _cb_tc0 = cb_watch_buzzer_seq; // setup TC0 timer _tc0_initialize(); - // TCC should run in standby mode - _tcc_write_RUNSTDBY(true); // start the timer (for the 64 hz callback) _tc0_start(); } @@ -110,51 +135,156 @@ void cb_watch_buzzer_seq(void) { // read note watch_buzzer_note_t note = _sequence[_seq_position]; if (note != BUZZER_NOTE_REST) { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], 25); + watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], _volume); watch_set_buzzer_on(); } else watch_set_buzzer_off(); // set duration ticks and move to next tone - _tone_ticks = _sequence[_seq_position + 1]; + _tone_ticks = _sequence[_seq_position + 1] - 1; _seq_position += 2; } else { // end the sequence watch_buzzer_abort_sequence(); - if (_cb_finished) _cb_finished(); } } else _tone_ticks--; } +void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end) { + watch_buzzer_play_raw_source_with_volume(raw_source, userdata, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); +} + +void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume) { + // Abort any previous sequence + watch_buzzer_abort_sequence(); + + if (_cb_start_global) { + _cb_start_global(); + } + + watch_enable_buzzer(); + + watch_set_buzzer_off(); + _raw_source = raw_source; + _userdata = userdata; + _cb_finished = callback_on_end; + _volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25; + _seq_position = 0; + _tone_ticks = 0; + // prepare buzzer + + _cb_tc0 = cb_watch_buzzer_raw_source; + // setup TC0 timer + _tc0_initialize(); + // start the timer (for the 64 hz callback) + _tc0_start(); +} + +void cb_watch_buzzer_raw_source(void) { + // callback for reading the note sequence + uint16_t period; + uint16_t duration; + bool done; + + if (_tone_ticks == 0) { + done = _raw_source(_seq_position, _userdata, &period, &duration); + + if (done || duration == 0) { + // end the sequence + watch_buzzer_abort_sequence(); + } else { + if (period == WATCH_BUZZER_PERIOD_REST) { + watch_set_buzzer_off(); + } else { + watch_set_buzzer_period_and_duty_cycle(period, _volume); + watch_set_buzzer_on(); + } + + // set duration ticks and move to next tone + _tone_ticks = duration - 1; + _seq_position += 1; + } + } else { + _tone_ticks--; + } +} + void watch_buzzer_abort_sequence(void) { // ends/aborts the sequence - if (_callback_running) _tc0_stop(); + if (!_buzzer_is_active) { + return; + } + + _tc0_stop(); + watch_set_buzzer_off(); - // disable standby mode for TCC - _tcc_write_RUNSTDBY(false); + + // disable TCC + watch_disable_buzzer(); + + if (_cb_stop_global) { + _cb_stop_global(); + } + + if (_cb_finished) { + _cb_finished(); + } +} + +void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop) { + _cb_stop_global = cb_start; + _cb_stop_global = cb_stop; } void irq_handler_tc0(void) { // interrupt handler for TC0 (globally!) - cb_watch_buzzer_seq(); + if (_cb_tc0) { + _cb_tc0(); + } TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; } -bool watch_is_buzzer_or_led_enabled(void){ - return tcc_is_enabled(0); +void _watch_maybe_enable_tcc(void) { + if (!_buzzer_is_active && !_led_is_active) { + return; + } + + if (!tcc_is_enabled(0)) { + // tcc_set_run_in_standby(0, true); + _watch_enable_tcc(); + // TCC should run in standby mode + _tcc_write_RUNSTDBY(true); + } } -inline void watch_enable_buzzer(void) { - if (!tcc_is_enabled(0)) { - _watch_enable_tcc(); +void _watch_maybe_disable_tcc(void) { + if (_buzzer_is_active || _led_is_active) { + return; } + + if (tcc_is_enabled(0)) { + _tcc_write_RUNSTDBY(false); + _watch_disable_tcc(); + } +} + +void watch_enable_buzzer(void) { + _buzzer_is_active = true; + _watch_maybe_enable_tcc(); +} + +void watch_disable_buzzer(void) { + _buzzer_is_active = false; + watch_set_buzzer_off(); + _watch_maybe_disable_tcc(); } void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) { tcc_set_period(0, period, true); tcc_set_cc(0, (WATCH_BUZZER_TCC_CHANNEL) % 4, period / (100 / duty), true); -} - -void watch_disable_buzzer(void) { - _watch_disable_tcc(); + // The buzzer determines the period, which means that if the LED was active before it will flicker + // Update the LED duty cycle to match the new period required by the buzzer. + if (_led_is_active) { + _watch_set_led_duty_cycle(period, _current_led_color[0], _current_led_color[1], _current_led_color[2]); + } } inline void watch_set_buzzer_on(void) { @@ -172,14 +302,17 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { } void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) { - if (note == BUZZER_NOTE_REST) { - watch_set_buzzer_off(); - } else { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25); - watch_set_buzzer_on(); - } - delay_ms(duration_ms); - watch_set_buzzer_off(); + static int8_t single_note_sequence[3]; + + single_note_sequence[0] = note; + // 64 ticks per second for the tc0 + // Each tick is approximately 15ms + uint16_t duration = duration_ms / 15; + if (duration > 127) duration = 127; + single_note_sequence[1] = (int8_t)duration; + single_note_sequence[2] = 0; + + watch_buzzer_play_sequence_with_volume(single_note_sequence, NULL, volume); } void _watch_enable_tcc(void) { @@ -220,21 +353,6 @@ void _watch_enable_tcc(void) { tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, 0, false); #endif - // enable LED PWM pins (the LED driver assumes if the TCC is on, the pins are enabled) - HAL_GPIO_RED_pmuxen(HAL_GPIO_PMUX_TCC_ALT); - HAL_GPIO_RED_drvstr(1); - HAL_GPIO_RED_out(); -#ifdef WATCH_GREEN_TCC_CHANNEL - HAL_GPIO_GREEN_pmuxen(HAL_GPIO_PMUX_TCC_ALT); - HAL_GPIO_GREEN_drvstr(1); - HAL_GPIO_GREEN_out(); -#endif -#ifdef WATCH_BLUE_TCC_CHANNEL - HAL_GPIO_BLUE_pmuxen(HAL_GPIO_PMUX_TCC_ALT); - HAL_GPIO_BLUE_drvstr(1); - HAL_GPIO_BLUE_out(); -#endif - // Enable the TCC tcc_enable(0); } @@ -257,13 +375,45 @@ void _watch_disable_tcc(void) { } void watch_enable_leds(void) { - if (!tcc_is_enabled(0)) { - _watch_enable_tcc(); - } + _led_is_active = true; + _watch_enable_led_pins(); + _watch_maybe_enable_tcc(); } void watch_disable_leds(void) { - _watch_disable_tcc(); + _led_is_active = false; + _watch_disable_led_pins(); + _watch_maybe_disable_tcc(); +} + +void _watch_enable_led_pins(void) { + // enable LED PWM pins (the LED driver assumes if the TCC is on, the pins are enabled) + HAL_GPIO_RED_pmuxen(HAL_GPIO_PMUX_TCC_ALT); + HAL_GPIO_RED_drvstr(1); + HAL_GPIO_RED_out(); +#ifdef WATCH_GREEN_TCC_CHANNEL + HAL_GPIO_GREEN_pmuxen(HAL_GPIO_PMUX_TCC_ALT); + HAL_GPIO_GREEN_drvstr(1); + HAL_GPIO_GREEN_out(); +#endif +#ifdef WATCH_BLUE_TCC_CHANNEL + HAL_GPIO_BLUE_pmuxen(HAL_GPIO_PMUX_TCC_ALT); + HAL_GPIO_BLUE_drvstr(1); + HAL_GPIO_BLUE_out(); +#endif +} + +void _watch_disable_led_pins(void) { + HAL_GPIO_RED_pmuxdis(); + HAL_GPIO_RED_off(); +#ifdef WATCH_GREEN_TCC_CHANNEL + HAL_GPIO_GREEN_pmuxdis(); + HAL_GPIO_GREEN_off(); +#endif +#ifdef WATCH_BLUE_TCC_CHANNEL + HAL_GPIO_BLUE_pmuxdis(); + HAL_GPIO_BLUE_off(); +#endif } void watch_set_led_color(uint8_t red, uint8_t green) { @@ -274,20 +424,35 @@ void watch_set_led_color(uint8_t red, uint8_t green) { #endif } -void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { - if (tcc_is_enabled(0)) { - uint32_t period = tcc_get_period(0); - tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true); +static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue) { + tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true); #ifdef WATCH_GREEN_TCC_CHANNEL - tcc_set_cc(0, (WATCH_GREEN_TCC_CHANNEL) % 4, ((period * (uint32_t)green * 1000ull) / 255000ull), true); + tcc_set_cc(0, (WATCH_GREEN_TCC_CHANNEL) % 4, ((period * (uint32_t)green * 1000ull) / 255000ull), true); #else - (void) green; // silence warning + (void) green; // silence warning #endif #ifdef WATCH_BLUE_TCC_CHANNEL - tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, ((period * (uint32_t)blue * 1000ull) / 255000ull), true); + tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, ((period * (uint32_t)blue * 1000ull) / 255000ull), true); #else - (void) blue; // silence warning + (void) blue; // silence warning #endif +} + +void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { + bool turning_on = (red | green | blue) != 0; + + if (turning_on) { + _current_led_color[0] = red; + _current_led_color[1] = green; + _current_led_color[2] = blue; + watch_enable_leds(); + uint32_t period = tcc_get_period(0); + _watch_set_led_duty_cycle(period, red, green, blue); + } else { + if (tcc_is_enabled(0)) { + _watch_set_led_duty_cycle(1, red, green, blue); + } + watch_disable_leds(); } } diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index b716f790..6f77a94e 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -277,20 +277,26 @@ inline void lis2dw_disable_fifo(void) { #endif } -bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data) { +bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data, uint32_t timeout) { + // timeout is in terms of 1/RTC_CNT_HZ seconds (likely 128 timeouts is one second) #ifdef I2C_SERCOM uint8_t temp = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_FIFO_SAMPLE); bool overrun = !!(temp & LIS2DW_FIFO_SAMPLE_OVERRUN); fifo_data->count = temp & LIS2DW_FIFO_SAMPLE_COUNT; + rtc_counter_t timeout_counter = watch_rtc_get_counter() + timeout; for(int i = 0; i < fifo_data->count; i++) { + if (watch_rtc_get_counter() > timeout_counter) { + break; + } fifo_data->readings[i] = lis2dw_get_raw_reading(); } return overrun; #else (void) fifo_data; + (void) timeout; return false; #endif } @@ -302,6 +308,20 @@ void lis2dw_clear_fifo(void) { #endif } +void lis2dw_enable_double_tap(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS, configuration | LIS2DW_WAKE_UP_THS_ENABLE_DOUBLE_TAP); +#endif +} + +void lis2dw_disable_double_tap(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS, configuration & ~LIS2DW_WAKE_UP_THS_ENABLE_DOUBLE_TAP); +#endif +} + void lis2dw_enable_sleep(void) { #ifdef I2C_SERCOM uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); @@ -397,6 +417,33 @@ void lis2dw_configure_int2(uint8_t sources) { #endif } +void lis2dw12_int_notification_set(lis2dw12_lir_t val) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3); + if (val == LIS2DW12_INT_LATCHED) { + configuration |= LIS2DW_CTRL3_VAL_LIR; + } else { + configuration &= ~LIS2DW_CTRL7_VAL_DRDY_PULSED; + } + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3, configuration); +#else + (void)val; +#endif +} + +lis2dw12_lir_t lis2dw12_int_notification_get(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3); + if (configuration & LIS2DW12_INT_LATCHED) { + return LIS2DW12_INT_LATCHED; + } else { + return LIS2DW12_INT_PULSED; + } +#else + return LIS2DW12_INT_PULSED; +#endif +} + void lis2dw_enable_interrupts(void) { #ifdef I2C_SERCOM uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); @@ -411,6 +458,20 @@ void lis2dw_disable_interrupts(void) { #endif } +void lis2dw_pulsed_drdy_interrupts(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration | LIS2DW_CTRL7_VAL_DRDY_PULSED); +#endif +} + +void lis2dw_latched_drdy_interrupts(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration & ~LIS2DW_CTRL7_VAL_DRDY_PULSED); +#endif +} + lis2dw_wakeup_source_t lis2dw_get_wakeup_source() { #ifdef I2C_SERCOM return (lis2dw_wakeup_source_t) watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_SRC); diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index 57ff05b2..fc5947e0 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -92,6 +92,12 @@ typedef enum { LIS2DW_FILTER_HIGH_PASS = 1, } lis2dw_filter_t; +typedef enum +{ + LIS2DW12_INT_PULSED = 0, + LIS2DW12_INT_LATCHED = 1, +} lis2dw12_lir_t; + typedef enum { LIS2DW_RANGE_16_G = 0b11, // +/- 16g LIS2DW_RANGE_8_G = 0b10, // +/- 8g @@ -295,6 +301,8 @@ typedef enum { #define LIS2DW_CTRL7_VAL_HP_REF_MODE 0b00000010 #define LIS2DW_CTRL7_VAL_LPASS_ON6D 0b00000001 +#define LIS2DW_FIFO_TIMEOUT 100 // timeout is in terms of 1/RTC_CNT_HZ seconds (likely 128 timeouts is one second) + bool lis2dw_begin(void); uint8_t lis2dw_get_device_id(void); @@ -339,10 +347,14 @@ void lis2dw_enable_fifo(void); void lis2dw_disable_fifo(void); -bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data); +bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data, uint32_t timeout); void lis2dw_clear_fifo(void); +void lis2dw_enable_double_tap(void); + +void lis2dw_disable_double_tap(void); + void lis2dw_enable_sleep(void); void lis2dw_disable_sleep(void); @@ -363,6 +375,10 @@ void lis2dw_configure_tap_threshold(uint8_t threshold_x, uint8_t threshold_y, ui void lis2dw_configure_tap_duration(uint8_t latency, uint8_t quiet, uint8_t shock); +void lis2dw12_int_notification_set(lis2dw12_lir_t val); + +lis2dw12_lir_t lis2dw12_int_notification_get(void); + void lis2dw_configure_int1(uint8_t sources); void lis2dw_configure_int2(uint8_t sources); @@ -371,6 +387,10 @@ void lis2dw_enable_interrupts(void); void lis2dw_disable_interrupts(void); +void lis2dw_pulsed_drdy_interrupts(void); + +void lis2dw_latched_drdy_interrupts(void); + lis2dw_interrupt_source_t lis2dw_get_interrupt_source(void); lis2dw_wakeup_source_t lis2dw_get_wakeup_source(void); diff --git a/watch-library/shared/watch/rtc32.h b/watch-library/shared/watch/rtc32.h new file mode 100644 index 00000000..52f20d68 --- /dev/null +++ b/watch-library/shared/watch/rtc32.h @@ -0,0 +1,98 @@ +////< @file rtc32.h +/* + * MIT License + * + * Copyright (c) 2020 Joey Castillo + * Copyright (c) 2025 Alessandro Genova + * + * 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 +#include + +/** + * @addtogroup rtc Real-Time Clock + * @brief Functions for configuring and using the Real-Time Clock peripheral. + * @details This is the rtc implementation for MODE0 (counter32) + * @{ + */ + +#define RTC_REFERENCE_YEAR (2020) + +typedef union { + struct { + uint32_t second : 6; // 0-59 + uint32_t minute : 6; // 0-59 + uint32_t hour : 5; // 0-23 + uint32_t day : 5; // 1-31 + uint32_t month : 4; // 1-12 + uint32_t year : 6; // 0-63 (representing 2020-2083) + } unit; + uint32_t reg; // the bit-packed value as expected by the RTC peripheral's CLOCK register. +} rtc_date_time_t; + +typedef enum rtc_alarm_match_t { + ALARM_MATCH_DISABLED = 0, + ALARM_MATCH_SS, + ALARM_MATCH_MMSS, + ALARM_MATCH_HHMMSS, +} rtc_alarm_match_t; + +typedef uint32_t rtc_counter_t; + +typedef void (*rtc_cb_t)(uint16_t intflag); + +/** @brief Initializes the RTC. + * @details Configures the RTC for COUNT32 mode, with a 1 Hz + * tick derived from the 1024 Hz clock on GCLK3 (for SAM D devices) + * or OSC32KCTRL's most accurate 1024 Hz output (for SAM L devices). + */ +void rtc_init(void); + +/** @brief Enables the RTC. + */ +void rtc_enable(void); + +/** @brief Checks if the RTC is enabled. + * @return true if the RTC is enabled; false if not. + */ +bool rtc_is_enabled(void); + +/** @brief Set the value of the counter register. + */ +void rtc_set_counter(rtc_counter_t counter); + +/** @brief Returns the value of the counter register. + */ +rtc_counter_t rtc_get_counter(void); + +/** @brief Configures the RTC alarm callback. + * @param callback The function to call when an RTC interrupt occurs. The callback + * will be passed a bitmask of the interrupt flags, the full contents + * of the RTC peripheral's INTFLAG register. + */ +void rtc_configure_callback(rtc_cb_t callback); + +void rtc_enable_compare_interrupt(uint32_t compare_time); +void rtc_disable_compare_interrupt(void); + +/** @} */ diff --git a/watch-library/shared/watch/watch_i2c.h b/watch-library/shared/watch/watch_i2c.h index fbcc1a92..18d07b38 100644 --- a/watch-library/shared/watch/watch_i2c.h +++ b/watch-library/shared/watch/watch_i2c.h @@ -45,22 +45,25 @@ void watch_disable_i2c(void); * @param addr The address of the device you wish to talk to. * @param buf A series of unsigned bytes; the data you wish to transmit. * @param length The number of bytes in buf that you wish to send. + * @return 0 if no error code, otherwise a code via i2c_result_t */ -void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length); +int8_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length); /** @brief Receives a series of values from a device on the I2C bus. * @param addr The address of the device you wish to hear from. * @param buf Storage for the incoming bytes; on return, it will contain the received data. * @param length The number of bytes that you wish to receive. + * @return 0 if no error code, otherwise a code via i2c_result_t */ -void watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length); +int8_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length); /** @brief Writes a byte to a register in an I2C device. * @param addr The address of the device you wish to address. * @param reg The register on the device that you wish to set. * @param data The value that you wish to set the register to. + * @return 0 if no error code, otherwise a code via i2c_result_t */ -void watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data); +int8_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data); /** @brief Reads a byte from a register in an I2C device. * @param addr The address of the device you wish to address. diff --git a/watch-library/shared/watch/watch_rtc.h b/watch-library/shared/watch/watch_rtc.h index a51fc826..504c37f3 100644 --- a/watch-library/shared/watch/watch_rtc.h +++ b/watch-library/shared/watch/watch_rtc.h @@ -27,7 +27,7 @@ ////< @file watch_rtc.h #include "watch.h" -#include "rtc.h" +#include "rtc32.h" /** @addtogroup rtc Real-Time Clock * @brief This section covers functions related to the SAM L22's real-time clock peripheral, including @@ -42,17 +42,20 @@ extern watch_cb_t btn_alarm_callback; extern watch_cb_t a2_callback; extern watch_cb_t a4_callback; +extern watch_cb_t comp_callback; #define WATCH_RTC_REFERENCE_YEAR (2020) #define watch_date_time_t rtc_date_time_t +typedef rtc_counter_t watch_counter_t; +typedef uint32_t unix_timestamp_t; /** @brief Called by main.c to check if the RTC is enabled. * You may call this function, but outside of app_init, it should always return true. */ bool _watch_rtc_is_enabled(void); -/** @brief Sets the date and time. +/** @brief Sets the date and time. Calls watch_rtc_set_unix_time internally. * @param date_time The date and time you wish to set, with a year value from 0-63 representing 2020-2083. * @note The SAM L22 stores the year as six bits representing a value from 0 to 63. It treats this as a year * offset from a reference year, which must be a leap year. Since 2020 was a leap year, and it allows @@ -62,7 +65,7 @@ bool _watch_rtc_is_enabled(void); */ void watch_rtc_set_date_time(rtc_date_time_t date_time); -/** @brief Returns the date and time. +/** @brief Returns the date and time. Calls watch_rtc_get_unix_time internally. * @return A rtc_date_time_t with the current date and time, with a year value from 0-63 representing 2020-2083. * @see watch_rtc_set_date_time for notes about how the year is stored. */ @@ -73,26 +76,79 @@ rtc_date_time_t watch_rtc_get_date_time(void); */ rtc_date_time_t watch_get_init_date_time(void); -/** @brief Registers an alarm callback that will be called when the RTC time matches the target time, as masked - * by the provided mask. - * @param callback The function you wish to have called when the alarm fires. If this value is NULL, the alarm +/** @brief Set the current UTC date and time using a unix timestamp + */ +void watch_rtc_set_unix_time(unix_timestamp_t unix_time); + +/** @brief Get the current UTC date and time using a unix timestamp + */ +unix_timestamp_t watch_rtc_get_unix_time(void); + +/** @brief Get the current value of the internal hardware counter + * @details The counter starts at 0 and it increases at a 128Hz rate until it overflows and starts over. + * We never manually set the counter. Doing so allows us to calculate absolute elapsed and more. + * When the user sets the time, what is modified is the reference time (i.e. the date and time when + * the counter is 0). + */ +rtc_counter_t watch_rtc_get_counter(void); + +/** @brief Get the RTC counter frequency. + */ +uint32_t watch_rtc_get_frequency(void); + +/** @brief Get how many counter ticks are in one minute. + */ +uint32_t watch_rtc_get_ticks_per_minute(void); + +/** @brief Registers a callback that will be called when the RTC counter matches the target counter. + * @param callback The function you wish to have called when the target counter is reached. If this value is NULL, the comp * interrupt will still be enabled, but no callback function will be called. - * @param alarm_time The time that you wish to match. The date is currently ignored. - * @param mask One of the values in rtc_alarm_match_t indicating which values to check. - * @details The alarm interrupt is a versatile tool for scheduling events in the future, especially since it can - * wake the device from all sleep modes. The key to its versatility is the mask parameter. - * Suppose we set an alarm for midnight, 00:00:00. - * * if mask is ALARM_MATCH_SS, the alarm will fire every minute when the clock ticks to seconds == 0. - * * with ALARM_MATCH_MMSS, the alarm will once an hour, at the top of each hour. - * * with ALARM_MATCH_HHMMSS, the alarm will fire at midnight every day. - * In theory the SAM L22's alarm function can match on days, months and even years, but I have not had - * success with this yet; as such, I am omitting these options for now. + * @param counter The time that you wish to match. The date is currently ignored. + * @param index We can have up to 8 active callbacks at a time. This parameter specifies which of the 8 callbacks should be set. + * @details The hardware RTC provides us with single interrupt that fires when the RTC counter matches a target counter COMP0. + * With a little bit of logic, we can provide multiple active compare callbacks. Every time a comp callback is + * registered/disabled/fired we iterate over all the active comp callbacks and set the hardware COMP0 counter + * to the next occurring one. + * With this very simple API, movement can implement one-shot timers to turn off the led and determine button longpresses + * as well as the inactivity timeouts for resigning and sleeping, as well as emulating the top of the minute alarm. */ -void watch_rtc_register_alarm_callback(watch_cb_t callback, rtc_date_time_t alarm_time, rtc_alarm_match_t mask); +void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index); + +/** @brief Just like watch_rtc_register_comp_callback but doesn't actually schedule the callback + * + * Useful if you need register multiple callbacks at once, avoids multiple calls to the expensive watch_rtc_schedule_next_comp: + * Usage: + * watch_rtc_register_comp_callback_no_schedule(cb0, counter0, index0); + * watch_rtc_register_comp_callback_no_schedule(cb1, counter1, index1); + * watch_rtc_schedule_next_comp(); + */ +void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index); + +/** @brief Disables the specified comp callback. + */ +void watch_rtc_disable_comp_callback(uint8_t index); + +/** @brief Just like watch_rtc_disable_comp_callback but doesn't actually schedule the callback + * + * Useful if you need disable multiple callbacks at once, avoids multiple calls to the expensive watch_rtc_schedule_next_comp: + * Usage: + * watch_rtc_disable_comp_callback_no_schedule(index0); + * watch_rtc_disable_comp_callback_no_schedule(index1); + * watch_rtc_schedule_next_comp(); + */ +/** @brief Disables the specified comp callback. + */ +void watch_rtc_disable_comp_callback_no_schedule(uint8_t index); + +/** @brief Determines the first comp callback that should fire and schedule it with the RTC + * + * You would never need to call this manually, unless you used the 'no_schedule' functions above. + */ +void watch_rtc_schedule_next_comp(void); /** @brief Disables the alarm callback. */ -void watch_rtc_disable_alarm_callback(void); +// void watch_rtc_disable_alarm_callback(void); /** @brief Registers a "tick" callback that will be called once per second. * @param callback The function you wish to have called when the clock ticks. If you pass in NULL, the tick @@ -117,10 +173,6 @@ void watch_rtc_disable_tick_callback(void); * tick at 16 or 32 Hz to update the screen more quickly. Just remember that the more frequent the tick, the more * power your app will consume. Ideally you should enable the fast tick only when the user requires it (i.e. in * response to an input event), and move back to the slow tick after some time. - * - * Also note that the RTC peripheral does not have sub-second resolution, so even if you set a 2 or 4 Hz interval, - * the system will not have any way of telling you where you are within a given second; watch_rtc_get_date_time - * will return the exact same timestamp until the second ticks over. */ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency); diff --git a/watch-library/shared/watch/watch_slcd.h b/watch-library/shared/watch/watch_slcd.h index b94b3f80..43a4f849 100644 --- a/watch-library/shared/watch/watch_slcd.h +++ b/watch-library/shared/watch/watch_slcd.h @@ -98,6 +98,10 @@ typedef enum { */ void watch_enable_display(void); +/** @brief Disables the Segment LCD display. + */ +void watch_disable_display(void); + /** @brief Sets a pixel. Use this to manually set a pixel with a given common and segment number. * See segmap.html. * @param com the common pin, numbered from 0-2. diff --git a/watch-library/shared/watch/watch_tcc.h b/watch-library/shared/watch/watch_tcc.h index aff88f7f..056844b3 100644 --- a/watch-library/shared/watch/watch_tcc.h +++ b/watch-library/shared/watch/watch_tcc.h @@ -126,12 +126,9 @@ typedef enum { BUZZER_NOTE_REST ///< no sound } watch_buzzer_note_t; -/** @brief Returns true if either the buzzer or the LED driver is enabled. - * @details Both the buzzer and the LED use the TCC peripheral to drive their behavior. This function returns true if that - * peripheral is enabled. You can use this function to determine whether you need to call the watch_enable_leds or - * or watch_enable_buzzer functions before using these peripherals. - */ -bool watch_is_buzzer_or_led_enabled(void); +#define WATCH_BUZZER_PERIOD_REST 0 + +typedef bool (*watch_buzzer_raw_source_t)(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration); /** @addtogroup tcc Buzzer and LED Control (via the TCC peripheral) * @brief This section covers functions related to Timer Counter for Control peripheral, which drives the piezo buzzer @@ -151,15 +148,13 @@ void watch_enable_buzzer(void); */ void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty); -/** @brief Disables the TCC peripheral that drives the buzzer. - * @note If you are using PWM to set custom LED colors, this method will also disable the LED PWM driver, - * since the buzzer and LED both make use of the same peripheral to drive their PWM behavior. +/** @brief Disables the TCC peripheral that drives the buzzer (if LED not active). */ void watch_disable_buzzer(void); /** @brief Turns the buzzer output on. It will emit a continuous sound at the given frequency. - * @note The TCC peripheral that drives the buzzer does not run in standby mode; if you wish for buzzer - * output to continue, you should prevent your app from going to sleep. + * @note The TCC peripheral that drives the buzzer does run in standby mode; if you wish for buzzer + * output to continue, you don't need to prevent your app from going to sleep. */ void watch_set_buzzer_on(void); @@ -170,8 +165,6 @@ void watch_set_buzzer_off(void); /** @brief Plays the given note for a set duration at the loudest possible volume. * @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration. * @param duration_ms The duration of the note. - * @note Note that this will block your UI for the duration of the note's play time, and it will - * after this call, the buzzer period will be set to the period of this note. */ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms); @@ -179,8 +172,6 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms); * @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration. * @param duration_ms The duration of the note. * @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD - * @note This will block your UI for the duration of the note's play time, and after this call, the - * buzzer will stop sounding, but the TCC period will remain set to the period of this note. */ void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume); @@ -202,10 +193,69 @@ extern const uint16_t NotePeriods[108]; */ void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)); +/** @brief Plays the given sequence of notes in a non-blocking way. + * @param note_sequence A pointer to the sequence of buzzer note & duration tuples, ending with a zero. A simple + * RLE logic is implemented: a negative number instead of a buzzer note means that the sequence + * is rewound by the given number of notes. The byte following a negative number determines the number + * of loops. I.e. if you want to repeat the last three notes of the sequence one time, you should provide + * the tuple -3, 1. The repeated notes must not contain any other repeat markers, or you will end up with + * an eternal loop. + * @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing. + * @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD + */ +void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume); + +/** @brief Plays the given raw buzzer source function in a non-blocking way. + * + * @details This function plays audio data generated by a raw source callback function, + * allowing for precise control over buzzer timing and frequency. The raw source + * function is called repeatedly to generate audio samples, each containing a + * period and duration for the buzzer tone. + * Useful for applications such as chirpy, so that they won't need to allocate a + * long note sequence, and we will also take care of all the timing logic. + * + * @param raw_source Pointer to the callback function that generates raw buzzer data. + * The function should take a position parameter and return true if + * more data is available, false if end of sequence is reached. + * Parameters: + * - position: Current position in the audio sequence (0-based) + * - userdata: User-provided data passed through to the callback + * - period: Pointer to store the period (in microseconds) for the tone + * - duration: Pointer to store the duration (in microseconds) for the tone + * @param userdata Pointer to user data that will be passed to the raw_source callback + * @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing. + */ +void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end); + +/** @brief Plays the given raw buzzer source function in a non-blocking way. + * + * @details This function plays audio data generated by a raw source callback function, + * allowing for precise control over buzzer timing and frequency. The raw source + * function is called repeatedly to generate audio samples, each containing a + * period and duration for the buzzer tone. + * Useful for applications such as chirpy, so that they won't need to allocate a + * long note sequence, and we will also take care of all the timing logic. + * + * @param raw_source Pointer to the callback function that generates raw buzzer data. + * The function should take a position parameter and return true if + * more data is available, false if end of sequence is reached. + * Parameters: + * - position: Current position in the audio sequence (0-based) + * - userdata: User-provided data passed through to the callback + * - period: Pointer to store the period (in microseconds) for the tone + * - duration: Pointer to store the duration (in microseconds) for the tone + * @param userdata Pointer to user data that will be passed to the raw_source callback + * @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing. + * @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD + */ +void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume); + /** @brief Aborts a playing sequence. */ void watch_buzzer_abort_sequence(void); +void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop); + #ifndef __EMSCRIPTEN__ void irq_handler_tc0(void); #endif @@ -223,26 +273,17 @@ void irq_handler_tc0(void); * so that watch_set_led_red sets the red LED, and watch_set_led_green sets the blue one. */ /// @{ -/** @brief Enables the bi-color LED. - * @note The TCC peripheral that drives the LEDs does not run in STANDBY mode — but the outputs do! This - * means that if you set either red, green or both LEDs to full power, they will shine even when - * your app is asleep. If, however, you set a custom color using watch_set_led_color, the color will - * not display correctly in STANDBY mode. You will need to keep your app running while the LED is on. +/** @brief Enables the TCC peripheral, which drives the LEDs. */ void watch_enable_leds(void); -/** @brief Disables the LEDs. - * @note This method will also disable the buzzer, since the buzzer and LED both make use of the same - * peripheral to drive their PWM behavior. +/** @brief Disables the TCC peripheral that drives the LEDs (if buzzer not active). */ void watch_disable_leds(void); /** @brief Sets the LED to a custom color by modulating each output's duty cycle. * @param red The red value from 0-255. * @param green The green value from 0-255. If your watch has a red/blue LED, this will be the blue value. - * @note If you are displaying a custom color, you will need to prevent your app from going to sleep - * while the LED is on; otherwise, the color will not display correctly. You can do this by - * returning false in your app_loop method. */ void watch_set_led_color(uint8_t red, uint8_t green); @@ -250,9 +291,6 @@ void watch_set_led_color(uint8_t red, uint8_t green); * @param red The red value from 0-255. * @param green The green value from 0-255. * @param blue The blue value from 0-255. - * @note If you are displaying a custom color, you will need to prevent your app from going to sleep - * while the LED is on; otherwise, the color will not display correctly. You can do this by - * returning false in your app_loop method. */ void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue); @@ -277,9 +315,6 @@ void watch_set_led_yellow(void); /** @brief Turns both the red and the green LEDs off. */ void watch_set_led_off(void); -/** @brief Disables the TCC peripheral. Should only be called internally. */ -void _watch_disable_tcc(void); - /// @brief An array of periods for all the notes on a piano, corresponding to the names in watch_buzzer_note_t. extern const uint16_t NotePeriods[108]; diff --git a/watch-library/shared/watch/watch_utility.c b/watch-library/shared/watch/watch_utility.c index 6d024032..d592fe56 100644 --- a/watch-library/shared/watch/watch_utility.c +++ b/watch-library/shared/watch/watch_utility.c @@ -205,7 +205,8 @@ uint32_t watch_utility_date_time_to_unix_time(watch_date_time_t date_time, int32 watch_date_time_t watch_utility_date_time_from_unix_time(uint32_t timestamp, int32_t utc_offset) { watch_date_time_t retval; retval.reg = 0; - int32_t days, secs; + uint32_t secs; + int32_t days; int32_t remdays, remsecs, remyears; int32_t qc_cycles, c_cycles, q_cycles; int32_t years, months; @@ -277,6 +278,10 @@ watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_ti return watch_utility_date_time_from_unix_time(timestamp, destination_utc_offset); } +uint32_t watch_utility_unix_time_convert_zone(uint32_t timestamp, uint32_t origin_utc_offset, uint32_t destination_utc_offset) { + return timestamp - origin_utc_offset + destination_utc_offset; +} + watch_duration_t watch_utility_seconds_to_duration(uint32_t seconds) { watch_duration_t retval; diff --git a/watch-library/shared/watch/watch_utility.h b/watch-library/shared/watch/watch_utility.h index 61c292b4..9b1b3a04 100644 --- a/watch-library/shared/watch/watch_utility.h +++ b/watch-library/shared/watch/watch_utility.h @@ -144,6 +144,16 @@ bool watch_utility_convert_to_12_hour(watch_date_time_t *date_time); */ watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_time, uint32_t origin_utc_offset, uint32_t destination_utc_offset); +/** @brief Converts a unix time from a given time zone to another time zone. + * @param timestamp The unix time that you wish to convert + * @param origin_utc_offset The number of seconds from UTC in the origin time zone + * @param destination_utc_offset The number of seconds from UTC in the destination time zone + * @return A unix time for the given UNIX timestamp and UTC offset. + * @note Adapted from MIT-licensed code from musl, Copyright © 2005-2014 Rich Felker, et al.: + * https://github.com/esmil/musl/blob/1cc81f5cb0df2b66a795ff0c26d7bbc4d16e13c6/src/time/__secs_to_tm.c + */ +uint32_t watch_utility_unix_time_convert_zone(uint32_t timestamp, uint32_t origin_utc_offset, uint32_t destination_utc_offset); + /** @brief Returns a temperature in degrees Celsius for a given thermistor voltage divider circuit. * @param value The raw analog reading from the thermistor pin (0-65535) * @param highside True if the thermistor is connected to VCC and the series resistor is connected diff --git a/watch-library/simulator/shell.html b/watch-library/simulator/shell.html index 8306b919..51d289cb 100644 --- a/watch-library/simulator/shell.html +++ b/watch-library/simulator/shell.html @@ -64,22 +64,22 @@

Sensor Watch Emulator

- - - - - - - + + + + + + + - - - - + + + diff --git a/watch-library/simulator/watch/watch.c b/watch-library/simulator/watch/watch.c index 4e89f3d3..7584cd38 100644 --- a/watch-library/simulator/watch/watch.c +++ b/watch-library/simulator/watch/watch.c @@ -1,9 +1,5 @@ #include "watch.h" -bool watch_is_buzzer_or_led_enabled(void) { - return false; -} - bool watch_is_usb_enabled(void) { return true; } diff --git a/watch-library/simulator/watch/watch_i2c.c b/watch-library/simulator/watch/watch_i2c.c index 09339888..8086fb75 100644 --- a/watch-library/simulator/watch/watch_i2c.c +++ b/watch-library/simulator/watch/watch_i2c.c @@ -28,11 +28,17 @@ void watch_enable_i2c(void) {} void watch_disable_i2c(void) {} -void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) {} +int8_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { + return 0; +} -void watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) {} +int8_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { + return 0; +} -void watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) {} +int8_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { + return 0; +} uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) { return 0; diff --git a/watch-library/simulator/watch/watch_private.c b/watch-library/simulator/watch/watch_private.c index 6f97490b..b94fbf12 100644 --- a/watch-library/simulator/watch/watch_private.c +++ b/watch-library/simulator/watch/watch_private.c @@ -51,8 +51,6 @@ int _gettimeofday(struct timeval *tv, void *tzvp) { return 0; } -void _watch_disable_tcc(void) {} - void _watch_enable_usb(void) {} void watch_disable_TRNG() {} diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index 40487dc6..93da5e6b 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -21,6 +21,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +#include +#include #include "watch_rtc.h" #include "watch_main_loop.h" @@ -29,8 +31,29 @@ #include #include +static const uint32_t RTC_CNT_HZ = 128; +static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1; +static const uint32_t RTC_CNT_DIV = 7; +static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; + +static bool rtc_enabled; +static uint32_t counter; +static uint32_t reference_timestamp; +static double next_tick_time; + +#define WATCH_RTC_N_COMP_CB 8 + +typedef struct { + volatile uint32_t counter; + volatile watch_cb_t callback; + volatile bool enabled; +} comp_cb_t; + static double time_offset = 0; -static long tick_callbacks[8] = { -1, -1, -1, -1, -1, -1, -1, -1 }; +watch_cb_t tick_callbacks[8]; +comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB]; + +static uint32_t scheduled_comp_counter; static long alarm_interval_id = -1; static long alarm_timeout_id = -1; @@ -40,41 +63,73 @@ watch_cb_t btn_alarm_callback; watch_cb_t a2_callback; watch_cb_t a4_callback; +static void _watch_increase_counter(void *userData); +static void _watch_process_periodic_callbacks(void); +static void _watch_process_comp_callbacks(void); + bool _watch_rtc_is_enabled(void) { - return true; + return rtc_enabled; } void _watch_rtc_init(void) { -#if EMSCRIPTEN - // Shifts the timezone so our local time is converted to UTC and set + for (uint8_t index = 0; index < 8; ++index) { + tick_callbacks[index] = NULL; + } + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + comp_callbacks[index].counter = 0; + comp_callbacks[index].callback = NULL; + comp_callbacks[index].enabled = false; + } + + scheduled_comp_counter = 0; + counter = 0; + rtc_enabled = false; + + watch_rtc_set_date_time(watch_get_init_date_time()); + watch_rtc_enable(true); +} + +void watch_rtc_set_date_time(rtc_date_time_t date_time) { + watch_rtc_set_unix_time(watch_utility_date_time_to_unix_time(date_time, 0)); +} + +rtc_date_time_t watch_rtc_get_date_time(void) { + return watch_utility_date_time_from_unix_time(watch_rtc_get_unix_time(), 0); +} + +void watch_rtc_set_unix_time(unix_timestamp_t unix_time) { + // unix_time = time_backup + counter / RTC_CNT_HZ - 0.5 + rtc_counter_t counter = watch_rtc_get_counter(); + reference_timestamp = unix_time - (counter >> RTC_CNT_DIV) - ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) + 1; +} + +unix_timestamp_t watch_rtc_get_unix_time(void) { + // unix_time = time_backup + counter / RTC_CNT_HZ - 0.5 + rtc_counter_t counter = watch_rtc_get_counter(); + return reference_timestamp + (counter >> RTC_CNT_DIV) + ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) - 1; +} + +rtc_counter_t watch_rtc_get_counter(void) { + return counter; +} + +uint32_t watch_rtc_get_frequency(void) { + return RTC_CNT_HZ; +} + +uint32_t watch_rtc_get_ticks_per_minute(void) { + return RTC_CNT_TICKS_PER_MINUTE; +} + +rtc_date_time_t watch_get_init_date_time(void) { + rtc_date_time_t date_time = {0}; + int32_t time_zone_offset = EM_ASM_INT({ - return -new Date().getTimezoneOffset() * 60; + return new Date().getTimezoneOffset() * 60 * 1000; // ms }); -#endif -#ifdef BUILD_YEAR - watch_date_time_t date_time = watch_get_init_date_time(); -#else - watch_date_time_t date_time = watch_rtc_get_date_time(); -#endif - watch_rtc_set_date_time(watch_utility_date_time_convert_zone(date_time, time_zone_offset, 0)); -} -void watch_rtc_set_date_time(watch_date_time_t date_time) { - time_offset = EM_ASM_DOUBLE({ - const year = 2020 + (($0 >> 26) & 0x3f); - const month = ($0 >> 22) & 0xf; - const day = ($0 >> 17) & 0x1f; - const hour = ($0 >> 12) & 0x1f; - const minute = ($0 >> 6) & 0x3f; - const second = $0 & 0x3f; - const date = new Date(year, month - 1, day, hour, minute, second); - return date - Date.now(); - }, date_time.reg); -} - -watch_date_time_t watch_rtc_get_date_time(void) { - watch_date_time_t retval; - retval.reg = EM_ASM_INT({ + date_time.reg = EM_ASM_INT({ const date = new Date(Date.now() + $0); return date.getSeconds() | (date.getMinutes() << 6) | @@ -82,27 +137,16 @@ watch_date_time_t watch_rtc_get_date_time(void) { (date.getDate() << 17) | ((date.getMonth() + 1) << 22) | ((date.getFullYear() - 2020) << 26); - }, time_offset); - return retval; -} - -rtc_date_time_t watch_get_init_date_time(void) { - rtc_date_time_t date_time = {0}; + }, time_zone_offset); #ifdef BUILD_YEAR date_time.unit.year = BUILD_YEAR; -#else - date_time.unit.year = 5; #endif #ifdef BUILD_MONTH date_time.unit.month = BUILD_MONTH; -#else - date_time.unit.month = 1; #endif #ifdef BUILD_DAY date_time.unit.day = BUILD_DAY; -#else - date_time.unit.day = 1; #endif #ifdef BUILD_HOUR date_time.unit.hour = BUILD_HOUR; @@ -122,10 +166,94 @@ void watch_rtc_disable_tick_callback(void) { watch_rtc_disable_periodic_callback(1); } -static void watch_invoke_periodic_callback(void *userData) { - watch_cb_t callback = userData; - callback(); +static void _watch_schedule_next_tick(void) { + if (!rtc_enabled) return; + + double now = EM_ASM_DOUBLE({ return performance.now(); }); + + // Target interval in ms + double ms = 1000.0 / (double)RTC_CNT_HZ; + + // Schedule next tick, correcting for drift + next_tick_time += ms; + double delay = next_tick_time - now; + + // If we're behind, reset timing + if (delay < 0) { + next_tick_time = now + ms; + delay = ms; + } + + emscripten_async_call(_watch_increase_counter, NULL, delay); +} + +static void _watch_increase_counter(void *userData) { + (void) userData; + + counter += 1; + // Fire the periodic callbacks that match this counter + _watch_process_periodic_callbacks(); + // Fire the comp callbacks that match this counter + _watch_process_comp_callbacks(); + resume_main_loop(); + + // Schedule the next tick with drift correction + _watch_schedule_next_tick(); +} + +static void _watch_process_periodic_callbacks(void) { + /* It looks weird but it follows the way the hardware triggers periodic interrupts. + * For 128hz counter periodic interrupts fire at these tick values: + * 1Hz: 64 + * 2Hz: 32, 96 + * 4Hz: 16, 48, 80, 112 + * 8Hz: 8, 24, 40, 56, 72, 88, 104, 120 + * 16Hz: 4, 12, 20, ..., 124 + * 32Hz: 2, 6, 10, ..., 126 + * 64Hz: 1, 3, 5, ..., 127 + * 128Hz: 0, 1, 2, ..., 127 + * + * Which means that only one periodic interrupt can fire for a given counter value + * (except 128Hz which can always fire) + */ + + uint32_t freq = watch_rtc_get_frequency(); + uint32_t subsecond_mask = freq - 1; + uint32_t subseconds = counter & subsecond_mask; + + // Find the firs non-zero bit in the counter, which can be used to determine the appropriate period (see table above). + uint8_t per_n = 0; + + for (uint8_t i = 0; i < 7; i++) { + if (subseconds & (1 << i)) { + per_n = i + 1; + break; + } + } + + if (tick_callbacks[per_n]) { + tick_callbacks[per_n](); + } + + // 128Hz is always a match + if (per_n != 0 && tick_callbacks[0]) { + tick_callbacks[0](); + } +} + +static void _watch_process_comp_callbacks(void) { + // In hardware the interrupt fires one tick after the matching counter + if (counter == (scheduled_comp_counter + 1)) { + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + if (comp_callbacks[index].enabled && scheduled_comp_counter == comp_callbacks[index].counter) { + comp_callbacks[index].enabled = false; + comp_callbacks[index].callback(); + } + } + + watch_rtc_schedule_next_comp(); + } } void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency) { @@ -138,26 +266,19 @@ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency // 0x01 (1 Hz) will have 7 leading zeros for PER7. 0xF0 (128 Hz) will have no leading zeroes for PER0. uint8_t per_n = __builtin_clz(tmp); - double interval = 1000.0 / frequency; // in msec - - if (tick_callbacks[per_n] != -1) emscripten_clear_interval(tick_callbacks[per_n]); - tick_callbacks[per_n] = emscripten_set_interval(watch_invoke_periodic_callback, interval, (void *)callback); + tick_callbacks[per_n] = callback; } void watch_rtc_disable_periodic_callback(uint8_t frequency) { if (__builtin_popcount(frequency) != 1) return; uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24); - if (tick_callbacks[per_n] != -1) { - emscripten_clear_interval(tick_callbacks[per_n]); - tick_callbacks[per_n] = -1; - } + tick_callbacks[per_n] = NULL; } void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) { for (int i = 0; i < 8; i++) { - if (tick_callbacks[i] != -1 && (mask & (1 << i)) != 0) { - emscripten_clear_interval(tick_callbacks[i]); - tick_callbacks[i] = -1; + if (tick_callbacks[i] && (mask & (1 << i)) != 0) { + tick_callbacks[i] = NULL; } } } @@ -166,81 +287,95 @@ void watch_rtc_disable_all_periodic_callbacks(void) { watch_rtc_disable_matching_periodic_callbacks(0xFF); } -static void watch_invoke_alarm_interval_callback(void *userData) { - if (alarm_callback) alarm_callback(); -} - -static void watch_invoke_alarm_callback(void *userData) { - if (alarm_callback) alarm_callback(); - alarm_interval_id = emscripten_set_interval(watch_invoke_alarm_interval_callback, alarm_interval, NULL); -} - -void watch_rtc_register_alarm_callback(watch_cb_t callback, watch_date_time_t alarm_time, rtc_alarm_match_t mask) { - watch_rtc_disable_alarm_callback(); - - switch (mask) { - case ALARM_MATCH_DISABLED: - return; - case ALARM_MATCH_SS: - alarm_interval = 60 * 1000; - break; - case ALARM_MATCH_MMSS: - alarm_interval = 60 * 60 * 1000; - break; - case ALARM_MATCH_HHMMSS: - alarm_interval = 60 * 60 * 60 * 1000; - break; +void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; } - double timeout = EM_ASM_DOUBLE({ - const now = Date.now(); - const date = new Date(now); + comp_callbacks[index].counter = counter; + comp_callbacks[index].callback = callback; + comp_callbacks[index].enabled = true; - const hour = ($0 >> 12) & 0x1f; - const minute = ($0 >> 6) & 0x3f; - const second = $0 & 0x3f; + watch_rtc_schedule_next_comp(); +} - if ($1 == 1) { // SS - if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1); - date.setSeconds(second); - } else if ($1 == 2) { // MMSS - if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1); - if (minute < date.getMinutes()) date.setHours(date.getHours() + 1); - date.setMinutes(minute, second); - } else if ($1 == 3) { // HHMMSS - if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1); - if (minute < date.getMinutes()) date.setHours(date.getHours() + 1); - if (hour < date.getHours()) date.setDate(date.getDate() + 1); - date.setHours(hour, minute, second); - } else { - throw 'Invalid alarm match mask'; +void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].counter = counter; + comp_callbacks[index].callback = callback; + comp_callbacks[index].enabled = true; +} + +void watch_rtc_disable_comp_callback(uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].enabled = false; + + watch_rtc_schedule_next_comp(); +} + +void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].enabled = false; +} + +void watch_rtc_schedule_next_comp(void) { + rtc_counter_t curr_counter = watch_rtc_get_counter(); + // If there is already a pending comp interrupt for this very tick, let it fire + // And this function will be called again as soon as the interrupt fires. + if (curr_counter == scheduled_comp_counter) { + return; + } + + // The soonest we can schedule is the next tick + curr_counter +=1; + + bool schedule_any = false; + rtc_counter_t comp_counter; + rtc_counter_t min_diff = UINT_MAX; + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + // rtc_counter_t diff = + if (comp_callbacks[index].enabled) { + rtc_counter_t diff = comp_callbacks[index].counter - curr_counter; + if (diff <= min_diff) { + min_diff = diff; + comp_counter = comp_callbacks[index].counter; + schedule_any = true; + } } - - return date - now; - }, alarm_time.reg, mask); - - alarm_callback = callback; - alarm_timeout_id = emscripten_set_timeout(watch_invoke_alarm_callback, timeout, NULL); -} - -void watch_rtc_disable_alarm_callback(void) { - alarm_callback = NULL; - alarm_interval = 0; - - if (alarm_timeout_id != -1) { - emscripten_clear_timeout(alarm_timeout_id); - alarm_timeout_id = -1; } - if (alarm_interval_id != -1) { - emscripten_clear_interval(alarm_interval_id); - alarm_interval_id = -1; + if (schedule_any) { + scheduled_comp_counter = comp_counter; + } else { + scheduled_comp_counter = curr_counter - 2; } } void watch_rtc_enable(bool en) { - //Not simulated + // Nothing to do cases + if ((en && rtc_enabled) || (!en && !rtc_enabled)) { + return; + } + + if (en) { + // Use drift-correcting timer instead of fixed setInterval + rtc_enabled = true; + next_tick_time = EM_ASM_DOUBLE({ return performance.now(); }); + _watch_schedule_next_tick(); + } else { + rtc_enabled = false; + } } void watch_rtc_freqcorr_write(int16_t value, int16_t sign) diff --git a/watch-library/simulator/watch/watch_slcd.c b/watch-library/simulator/watch/watch_slcd.c index 34d8878c..1990e90c 100644 --- a/watch-library/simulator/watch/watch_slcd.c +++ b/watch-library/simulator/watch/watch_slcd.c @@ -50,17 +50,23 @@ void watch_enable_display(void) { _watch_update_indicator_segments(); #endif - EM_ASM({ #if defined(FORCE_CUSTOM_LCD_TYPE) - document.getElementById("classic").style.display = "none"; + EM_ASM({document.getElementById("custom").style.display = "";}); + EM_ASM({document.getElementById("classic").style.display = "none";}); #else - document.getElementById("custom").style.display = "none"; + EM_ASM({document.getElementById("custom").style.display = "none";}); + EM_ASM({document.getElementById("classic").style.display = "";}); #endif - }); watch_clear_display(); } +void watch_disable_display(void) { + watch_clear_display(); + EM_ASM({document.getElementById("classic").style.display = "none";}); + EM_ASM({document.getElementById("custom").style.display = "none";}); +} + void watch_set_pixel(uint8_t com, uint8_t seg) { EM_ASM({ document.querySelectorAll("[data-com='" + $0 + "'][data-seg='" + $1 + "']") diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index f0046849..e90b7613 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -28,18 +28,23 @@ #include #include -static bool buzzer_enabled = false; +static volatile bool buzzer_enabled = false; static uint32_t buzzer_period; void cb_watch_buzzer_seq(void *userData); +void cb_watch_buzzer_raw_source(void *userData); static uint16_t _seq_position; static int8_t _tone_ticks, _repeat_counter; -static long _em_interval_id = 0; +static volatile long _em_interval_id = 0; static int8_t *_sequence; +static watch_buzzer_raw_source_t _raw_source; +static void* _userdata; +static uint8_t _volume; static void (*_cb_finished)(void); - -void _watch_enable_tcc(void) {} +static watch_cb_t _cb_start_global = NULL; +static watch_cb_t _cb_stop_global = NULL; +static volatile bool _buzzer_is_active = false; static inline void _em_interval_stop() { emscripten_clear_interval(_em_interval_id); @@ -47,15 +52,28 @@ static inline void _em_interval_stop() { } void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) { - if (_em_interval_id) _em_interval_stop(); + watch_buzzer_play_sequence_with_volume(note_sequence, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); +} + +void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) { + watch_buzzer_abort_sequence(); + + // prepare buzzer + watch_enable_buzzer(); watch_set_buzzer_off(); + + _buzzer_is_active = true; + + if (_cb_start_global) { + _cb_start_global(); + } + _sequence = note_sequence; _cb_finished = callback_on_end; + _volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25; _seq_position = 0; _tone_ticks = 0; _repeat_counter = -1; - // prepare buzzer - watch_enable_buzzer(); // initiate 64 hz callback _em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL); } @@ -88,32 +106,115 @@ void cb_watch_buzzer_seq(void *userData) { if (note == BUZZER_NOTE_REST) { watch_set_buzzer_off(); } else { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], 25); + watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], _volume); watch_set_buzzer_on(); } // set duration ticks and move to next tone - _tone_ticks = _sequence[_seq_position + 1]; + _tone_ticks = _sequence[_seq_position + 1] - 1; _seq_position += 2; } else { // end the sequence watch_buzzer_abort_sequence(); - if (_cb_finished) _cb_finished(); } } else _tone_ticks--; } +void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end) { + watch_buzzer_play_raw_source_with_volume(raw_source, userdata, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); +} + +void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume) { + watch_buzzer_abort_sequence(); + + // prepare buzzer + watch_enable_buzzer(); + watch_set_buzzer_off(); + + _buzzer_is_active = true; + + if (_cb_start_global) { + _cb_start_global(); + } + + _raw_source = raw_source; + _userdata = userdata; + _cb_finished = callback_on_end; + _volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25; + _seq_position = 0; + _tone_ticks = 0; + + // initiate 64 hz callback + _em_interval_id = emscripten_set_interval(cb_watch_buzzer_raw_source, (double)(1000/64), (void *)NULL); +} + +void cb_watch_buzzer_raw_source(void *userData) { + // callback for reading the note sequence + (void) userData; + uint16_t period; + uint16_t duration; + bool done; + + if (_tone_ticks == 0) { + done = _raw_source(_seq_position, _userdata, &period, &duration); + + if (done || duration == 0) { + // end the sequence + watch_buzzer_abort_sequence(); + } else { + if (period == WATCH_BUZZER_PERIOD_REST) { + watch_set_buzzer_off(); + } else { + watch_set_buzzer_period_and_duty_cycle(period, _volume); + watch_set_buzzer_on(); + } + + // set duration ticks and move to next tone + _tone_ticks = duration - 1; + _seq_position += 1; + } + } else { + _tone_ticks--; + } +} + void watch_buzzer_abort_sequence(void) { // ends/aborts the sequence if (_em_interval_id) _em_interval_stop(); + watch_set_buzzer_off(); + watch_disable_buzzer(); + + if (!_buzzer_is_active) { + return; + } + + _buzzer_is_active = false; + + if (_cb_stop_global) { + _cb_stop_global(); + } + + if (_cb_finished) { + _cb_finished(); + } +} + +void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop) { + _cb_stop_global = cb_start; + _cb_stop_global = cb_stop; } void watch_enable_buzzer(void) { + watch_buzzer_abort_sequence(); buzzer_enabled = true; buzzer_period = NotePeriods[BUZZER_NOTE_A4]; EM_ASM({ - Module['audioContext'] = new (window.AudioContext || window.webkitAudioContext)(); + // "It's recommended to create one AudioContext and reuse it instead of initializing a new one each time." + // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext + if (!Module['audioContext']) { + Module['audioContext'] = new (window.AudioContext || window.webkitAudioContext)(); + } }); } @@ -126,13 +227,6 @@ void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty_cycle) void watch_disable_buzzer(void) { buzzer_enabled = false; buzzer_period = NotePeriods[BUZZER_NOTE_A4]; - - EM_ASM({ - if (Module['audioContext']) { - Module['audioContext'].close(); - Module['audioContext'] = null; - } - }); } void watch_set_buzzer_on(void) { @@ -175,52 +269,46 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { } void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) { - if (note == BUZZER_NOTE_REST) { - watch_set_buzzer_off(); - } else { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25); - watch_set_buzzer_on(); - } + static int8_t single_note_sequence[3]; - main_loop_sleep(duration_ms); - watch_set_buzzer_off(); + single_note_sequence[0] = note; + // 64 ticks per second for the tc0 + // Each tick is approximately 15ms + uint16_t duration = duration_ms / 15; + if (duration > 127) duration = 127; + single_note_sequence[1] = (int8_t)duration; + single_note_sequence[2] = 0; + + watch_buzzer_play_sequence_with_volume(single_note_sequence, NULL, volume); } void watch_enable_leds(void) {} void watch_disable_leds(void) {} -void watch_set_led_color(uint8_t red, uint8_t green) { +void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { EM_ASM({ - // the watch svg contains an feColorMatrix filter with id ledcolor - // and a green svg gradient that mimics the led being on - // https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feColorMatrix - // this changes the color of the gradient to match the red+green combination let filter = document.getElementById("ledcolor"); let color_matrix = filter.children[0].values.baseVal; - color_matrix[1].value = $0 / 255; // red value - color_matrix[6].value = $1 / 255; // green value - document.getElementById('light').style.opacity = Math.min(255, $0 + $1) / 255; - }, red, green); -} - -void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { - (void) blue; - watch_set_led_color(red, green); + color_matrix[0].value = $0 / 255; // red + color_matrix[6].value = $1 / 255; // green + color_matrix[12].value = $2 / 255; // blue + document.getElementById('light').style.opacity = Math.min(255, $0 + $1 + $2) / 255; + }, red, green, blue); } void watch_set_led_red(void) { - watch_set_led_color(255, 0); + watch_set_led_color_rgb(255, 0, 0); } void watch_set_led_green(void) { - watch_set_led_color(0, 255); + watch_set_led_color_rgb(0, 255, 0); } void watch_set_led_yellow(void) { - watch_set_led_color(255, 255); + watch_set_led_color_rgb(255, 255, 0); } void watch_set_led_off(void) { - watch_set_led_color(0, 0); + watch_set_led_color_rgb(0, 0, 0); }