Merge branch 'main' into tide_face

This commit is contained in:
voloved
2026-04-01 16:40:57 -04:00
committed by GitHub
77 changed files with 5915 additions and 1432 deletions
+4
View File
@@ -20,6 +20,9 @@ TINYUSB_CDC=1
# Now we're all set to include gossamer's make rules. # Now we're all set to include gossamer's make rules.
include $(GOSSAMER_PATH)/make.mk 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 CFLAGS+=-D_POSIX_C_SOURCE=200112L
define n define n
@@ -136,6 +139,7 @@ INCLUDES += \
-I./watch-library/hardware/watch \ -I./watch-library/hardware/watch \
SRCS += \ SRCS += \
./watch-library/hardware/watch/rtc32.c \
./watch-library/hardware/watch/watch.c \ ./watch-library/hardware/watch/watch.c \
./watch-library/hardware/watch/watch_adc.c \ ./watch-library/hardware/watch/watch_adc.c \
./watch-library/hardware/watch/watch_deepsleep.c \ ./watch-library/hardware/watch/watch_deepsleep.c \
@@ -28,7 +28,6 @@
#include "world_clock2_face.h" #include "world_clock2_face.h"
#include "watch.h" #include "watch.h"
#include "watch_utility.h" #include "watch_utility.h"
#include "watch_utility.h"
static bool refresh_face; static bool refresh_face;
@@ -448,13 +448,13 @@ static void start_reading(accelerometer_data_acquisition_state_t *state) {
state->records[state->pos++] = record; state->records[state->pos++] = record;
lis2dw_fifo_t fifo; 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) { static void continue_reading(accelerometer_data_acquisition_state_t *state) {
printf("Continue reading\n"); printf("Continue reading\n");
lis2dw_fifo_t fifo; 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. 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. uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second.
+734 -285
View File
File diff suppressed because it is too large Load Diff
+41 -23
View File
@@ -2,6 +2,7 @@
* MIT License * MIT License
* *
* Copyright (c) 2022 Joey Castillo * Copyright (c) 2022 Joey Castillo
* Copyright (c) 2025 Alessandro Genova
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * 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_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_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_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_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_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_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_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_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_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_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_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_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_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. EVENT_DOUBLE_TAP, // Accelerometer detected a double tap. This event is not yet implemented.
} movement_event_type_t; } 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 { typedef struct {
uint8_t event_type; uint8_t event_type;
uint8_t subsecond; uint8_t subsecond;
@@ -249,37 +273,16 @@ typedef struct {
int16_t current_face_idx; int16_t current_face_idx;
int16_t next_face_idx; int16_t next_face_idx;
bool watch_face_changed; bool watch_face_changed;
bool fast_tick_enabled;
int16_t fast_ticks;
// LED stuff // LED stuff
int16_t light_ticks; bool light_on;
// 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;
// background task handling // background task handling
bool woke_from_alarm_handler;
bool has_scheduled_background_task; 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 // stuff for subsecond tracking
uint8_t tick_frequency; uint8_t tick_frequency;
uint8_t last_second; uint8_t tick_pern;
uint8_t subsecond;
// backup register stuff // backup register stuff
uint8_t next_available_backup_register; uint8_t next_available_backup_register;
@@ -296,6 +299,10 @@ typedef struct {
lis2dw_data_rate_t accelerometer_background_rate; lis2dw_data_rate_t accelerometer_background_rate;
// threshold for considering the wearer is in motion // threshold for considering the wearer is in motion
uint8_t accelerometer_motion_threshold; uint8_t accelerometer_motion_threshold;
// signal and alarm volumes
watch_buzzer_volume_t signal_volume;
watch_buzzer_volume_t alarm_volume;
} movement_state_t; } movement_state_t;
void movement_move_to_face(uint8_t watch_face_index); 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_sleep(void);
void movement_request_wake(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_signal(void);
void movement_play_alarm(void); void movement_play_alarm(void);
void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note); 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); 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_utc_date_time(void);
watch_date_time_t movement_get_local_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); 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_local_date_time(watch_date_time_t date_time);
void movement_set_utc_timestamp(uint32_t timestamp);
bool movement_button_should_sound(void); bool movement_button_should_sound(void);
void movement_set_button_should_sound(bool value); 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); watch_buzzer_volume_t movement_button_volume(void);
void movement_set_button_volume(watch_buzzer_volume_t value); 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); movement_clock_mode_t movement_clock_mode_24h(void);
void movement_set_clock_mode_24h(movement_clock_mode_t value); void movement_set_clock_mode_24h(movement_clock_mode_t value);
+11 -3
View File
@@ -32,13 +32,13 @@ const watch_face_t watch_faces[] = {
world_clock_face, world_clock_face,
sunrise_sunset_face, sunrise_sunset_face,
moon_phase_face, moon_phase_face,
stopwatch_face, fast_stopwatch_face,
countdown_face, countdown_face,
alarm_face, alarm_face,
temperature_display_face, temperature_display_face,
voltage_face, voltage_face,
settings_face, settings_face,
set_time_face set_time_face,
}; };
#define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) #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. * 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. * 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. */ /* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */
#define SIGNAL_TUNE_DEFAULT #define SIGNAL_TUNE_DEFAULT
@@ -68,6 +68,8 @@ const watch_face_t watch_faces[] = {
#define MOVEMENT_DEFAULT_BUTTON_SOUND true #define MOVEMENT_DEFAULT_BUTTON_SOUND true
#define MOVEMENT_DEFAULT_BUTTON_VOLUME WATCH_BUZZER_VOLUME_SOFT #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 /* Set the timeout before switching back to the main watch face
* Valid values are: * Valid values are:
@@ -100,4 +102,10 @@ const watch_face_t watch_faces[] = {
*/ */
#define MOVEMENT_DEFAULT_LED_DURATION 1 #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_ #endif // MOVEMENT_CONFIG_H_
+9
View File
@@ -73,5 +73,14 @@
#include "wareki_face.h" #include "wareki_face.h"
#include "deadline_face.h" #include "deadline_face.h"
#include "wordle_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" #include "tide_face.h"
// New includes go above this line. // New includes go above this line.
+9
View File
@@ -22,6 +22,7 @@ SRCS += \
./watch-faces/demo/character_set_face.c \ ./watch-faces/demo/character_set_face.c \
./watch-faces/demo/light_sensor_face.c \ ./watch-faces/demo/light_sensor_face.c \
./watch-faces/demo/peek_memory_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/accelerometer_status_face.c \
./watch-faces/sensor/temperature_display_face.c \ ./watch-faces/sensor/temperature_display_face.c \
./watch-faces/sensor/temperature_logging_face.c \ ./watch-faces/sensor/temperature_logging_face.c \
@@ -48,5 +49,13 @@ SRCS += \
./watch-faces/sensor/lis2dw_monitor_face.c \ ./watch-faces/sensor/lis2dw_monitor_face.c \
./watch-faces/complication/wareki_face.c \ ./watch-faces/complication/wareki_face.c \
./watch-faces/complication/deadline_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 \ ./watch-faces/complication/tide_face.c \
# New watch faces go above this line. # New watch faces go above this line.
+236
View File
@@ -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 <stdlib.h>
#include <string.h>
#include <math.h>
#include "solar_time_face.h"
#include "watch.h"
#include "watch_utility.h"
#include "filesystem.h"
#if __EMSCRIPTEN__
#include <emscripten.h>
#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();
}
+84
View File
@@ -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, \
})
+49 -25
View File
@@ -40,9 +40,9 @@ typedef enum {
alarm_setting_idx_beeps alarm_setting_idx_beeps
} alarm_setting_idx_t; } 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 char _dow_strings_classic[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SU", "ED", "1t", "MF", "WN"};
static const uint8_t _blink_idx[ALARM_SETTING_STATES] = {2, 0, 4, 6, 8, 9}; 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 _blink_idx2[ALARM_SETTING_STATES] = {3, 1, 5, 7, 8, 9}; 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}; 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 // 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); 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) { static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) {
char buf[12]; char buf[12];
bool set_leading_zero = movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_024H; 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); 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 // blink items if in settings mode
if (state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks) { bool blinking = 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]] = ' '; 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) { if (state->is_setting) {
watch_display_text(WATCH_POSITION_SECONDS, " ");
// draw pitch level indicator // draw pitch level indicator
if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_pitch)) { if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_pitch)) {
for (i = 0; i <= state->alarm[state->alarm_idx].pitch && i < 3; i++) 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 // draw beep rounds indicator
if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_beeps)) { if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_beeps)) {
if (state->alarm[state->alarm_idx].beeps == ALARM_MAX_BEEP_ROUNDS - 1) 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 { else {
if (state->alarm[state->alarm_idx].beeps == 0) 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 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 // set alarm indicator
_alarm_set_signal(state); _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) { static void _alarm_play_short_beep(uint8_t pitch_idx) {
// play a short double beep // play a short double beep
watch_buzzer_play_note(_buzzer_notes[pitch_idx], 50); static int8_t beep_sequence[] = {
watch_buzzer_play_note(BUZZER_NOTE_REST, 50); 0, 4,
watch_buzzer_play_note(_buzzer_notes[pitch_idx], 70); 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) { 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 // revert change of enabled flag and show it briefly
state->alarm[state->alarm_idx].enabled ^= 1; state->alarm[state->alarm_idx].enabled ^= 1;
_alarm_set_signal(state); _alarm_set_signal(state);
_alarm_show_alarm_on_text(state);
delay_ms(275); delay_ms(275);
state->alarm_idx = 0; state->alarm_idx = 0;
} }
@@ -413,14 +444,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) {
// play alarm // play alarm
if (state->alarm[state->alarm_playing_idx].beeps == 0) { if (state->alarm[state->alarm_playing_idx].beeps == 0) {
// short beep // short beep
if (watch_is_buzzer_or_led_enabled()) { _alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch);
_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();
}
} else { } else {
// regular alarm beeps // 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), movement_play_alarm_beeps((state->alarm[state->alarm_playing_idx].beeps == (ALARM_MAX_BEEP_ROUNDS - 1) ? 20 : state->alarm[state->alarm_playing_idx].beeps),
+2 -2
View File
@@ -210,7 +210,7 @@ static inline void _display_counts(baby_kicks_state_t *state) {
watch_display_text(WATCH_POSITION_BOTTOM, "baby "); watch_display_text(WATCH_POSITION_BOTTOM, "baby ");
watch_clear_colon(); watch_clear_colon();
} else { } else {
char buf[7]; char buf[9];
snprintf( snprintf(
buf, buf,
@@ -259,7 +259,7 @@ static void _display_elapsed_minutes(baby_kicks_state_t *state) {
* the total elapsed minutes. * the total elapsed minutes.
*/ */
char buf[3]; char buf[5];
uint32_t elapsed_minutes = _elapsed_minutes(state); uint32_t elapsed_minutes = _elapsed_minutes(state);
uint8_t multiple = elapsed_minutes / 30; uint8_t multiple = elapsed_minutes / 30;
uint8_t remainder = elapsed_minutes % 30; uint8_t remainder = elapsed_minutes % 30;
+472
View File
@@ -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 <time.h>
#endif
#include <stdlib.h>
#include <string.h>
#include "blackjack_face.h"
#include "watch_common_display.h"
#define ACE 14
#define KING 13
#define QUEEN 12
#define JACK 11
#define MIN_CARD_VALUE 2
#define MAX_CARD_VALUE ACE
#define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1)
#define CARD_SUIT_COUNT 4
#define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT)
#define BLACKJACK_MAX_HAND_SIZE 11 // 4*1 + 4*2 + 3*3 = 21; 11 cards total
#define MAX_PLAYER_CARDS_DISPLAY 4
#define BOARD_DISPLAY_START 4
typedef struct {
uint8_t score;
uint8_t idx_hand;
int8_t high_aces_in_hand;
uint8_t hand[BLACKJACK_MAX_HAND_SIZE];
} hand_info_t;
typedef enum {
BJ_TITLE_SCREEN,
BJ_PLAYING,
BJ_DEALER_PLAYING,
BJ_BUST,
BJ_RESULT,
BJ_WIN_RATIO,
} game_state_t;
typedef enum {
A, B, C, D, E, F, G
} segment_t;
static bool tap_turned_on = false;
static game_state_t game_state;
static uint8_t deck[DECK_SIZE] = {0};
static uint8_t current_card = 0;
static blackjack_face_state_t *g_state = NULL;
hand_info_t player;
hand_info_t dealer;
static uint8_t generate_random_number(uint8_t num_values) {
// Emulator: use rand. Hardware: use arc4random.
#if __EMSCRIPTEN__
return rand() % num_values;
#else
return arc4random_uniform(num_values);
#endif
}
static void stack_deck(void) {
for (size_t i = 0; i < CARD_RANK_COUNT; i++) {
for (size_t j = 0; j < CARD_SUIT_COUNT; j++)
deck[(i * CARD_SUIT_COUNT) + j] = MIN_CARD_VALUE + i;
}
}
static void shuffle_deck(void) {
// Randomize shuffle with Fisher Yates
size_t i;
size_t j;
uint8_t tmp;
for (i = DECK_SIZE - 1; i > 0; i--) {
j = generate_random_number(0xFF) % (i + 1);
tmp = deck[j];
deck[j] = deck[i];
deck[i] = tmp;
}
}
static void reset_deck(void) {
current_card = 0;
shuffle_deck();
}
static uint8_t get_next_card(void) {
if (current_card >= DECK_SIZE)
reset_deck();
return deck[current_card++];
}
static uint8_t get_card_value(uint8_t card) {
switch (card)
{
case ACE:
return 11;
case KING:
case QUEEN:
case JACK:
return 10;
default:
return card;
}
}
static void modify_score_from_aces(hand_info_t *hand_info) {
while (hand_info->score > 21 && hand_info->high_aces_in_hand > 0) {
hand_info->score -= 10;
hand_info->high_aces_in_hand--;
}
}
static void reset_hands(void) {
memset(&player, 0, sizeof(player));
memset(&dealer, 0, sizeof(dealer));
reset_deck();
}
static void give_card(hand_info_t *hand_info) {
uint8_t card = get_next_card();
if (card == ACE) hand_info->high_aces_in_hand++;
hand_info->hand[hand_info->idx_hand++] = card;
uint8_t card_value = get_card_value(card);
hand_info->score += card_value;
modify_score_from_aces(hand_info);
}
static void set_segment_at_position(segment_t segment, uint8_t position) {
digit_mapping_t segmap;
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) {
segmap = Custom_LCD_Display_Mapping[position];
} else {
segmap = Classic_LCD_Display_Mapping[position];
}
const uint8_t com_pin = segmap.segment[segment].address.com;
const uint8_t seg = segmap.segment[segment].address.seg;
watch_set_pixel(com_pin, seg);
}
static void display_card_at_position(uint8_t card, uint8_t display_position) {
switch (card) {
case KING:
watch_display_character(' ', display_position);
set_segment_at_position(A, display_position);
set_segment_at_position(D, display_position);
set_segment_at_position(G, display_position);
break;
case QUEEN:
watch_display_character(' ', display_position);
set_segment_at_position(A, display_position);
set_segment_at_position(D, display_position);
break;
case JACK:
watch_display_character('-', display_position);
break;
case ACE:
watch_display_character(watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM ? 'A' : 'a', display_position);
break;
case 10:
watch_display_character('0', display_position);
break;
default: {
const char display_char = card + '0';
watch_display_character(display_char, display_position);
break;
}
}
}
static void display_player_hand(void) {
uint8_t card;
if (player.idx_hand <= MAX_PLAYER_CARDS_DISPLAY) {
card = player.hand[player.idx_hand - 1];
display_card_at_position(card, BOARD_DISPLAY_START + player.idx_hand - 1);
} else {
for (uint8_t i=0; i<MAX_PLAYER_CARDS_DISPLAY; i++) {
card = player.hand[player.idx_hand - MAX_PLAYER_CARDS_DISPLAY + i];
display_card_at_position(card, BOARD_DISPLAY_START + i);
}
}
}
static void display_dealer_hand(void) {
uint8_t card = dealer.hand[dealer.idx_hand - 1];
display_card_at_position(card, 0);
}
static void display_score(uint8_t score, watch_position_t pos) {
char buf[4];
sprintf(buf, "%2d", score);
watch_display_text(pos, buf);
}
static void add_to_game_scores(bool add_to_wins) {
g_state->games_played++;
if (g_state->games_played == 0) {
// Overflow
g_state->games_played = 1;
g_state->games_won = add_to_wins ? 1 : 0;
return;
}
if (add_to_wins) {
g_state->games_won++;
if (g_state->games_won == 0) {
// Overflow
g_state->games_played = 1;
g_state->games_won = 1;
}
}
}
static void display_win(void) {
game_state = BJ_RESULT;
add_to_game_scores(true);
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WlN ", " WIN");
display_score(player.score, WATCH_POSITION_SECONDS);
display_score(dealer.score, WATCH_POSITION_TOP_RIGHT);
}
static void display_lose(void) {
game_state = BJ_RESULT;
add_to_game_scores(false);
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "LOSE", "lOSE");
display_score(player.score, WATCH_POSITION_SECONDS);
display_score(dealer.score, WATCH_POSITION_TOP_RIGHT);
}
static void display_tie(void) {
game_state = BJ_RESULT;
// Don't record ties to the win ratio
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "TlE ", " TIE");
display_score(player.score, WATCH_POSITION_SECONDS);
}
static void display_bust(void) {
game_state = BJ_RESULT;
add_to_game_scores(false);
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST", "BUST");
}
static void display_title(void) {
game_state = BJ_TITLE_SCREEN;
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text_with_fallback(WATCH_POSITION_TOP, "BLACK ", "21");
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " JACK ", "BLaKJK");
}
static void display_win_ratio(blackjack_face_state_t *state) {
char buf[7];
game_state = BJ_WIN_RATIO;
uint8_t win_ratio = 0;
if (state->games_played > 0) { // Avoid dividing by zero
win_ratio = (uint8_t)((100 * state->games_won) / state->games_played);
}
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text_with_fallback(WATCH_POSITION_TOP, "WINS ", "WR");
sprintf(buf, "%3dPct", win_ratio);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
static void begin_playing(bool tap_control_on) {
watch_clear_display();
if (tap_control_on) {
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
}
game_state = BJ_PLAYING;
reset_hands();
// Give player their first 2 cards
give_card(&player);
display_player_hand();
give_card(&player);
display_player_hand();
display_score(player.score, WATCH_POSITION_SECONDS);
give_card(&dealer);
display_dealer_hand();
display_score(dealer.score, WATCH_POSITION_TOP_RIGHT);
}
static void perform_stand(void) {
game_state = BJ_DEALER_PLAYING;
watch_display_text(WATCH_POSITION_BOTTOM, "Stnd");
display_score(player.score, WATCH_POSITION_SECONDS);
}
static void perform_hit(void) {
if (player.score == 21) {
perform_stand();
return; // Assume hitting on 21 is a mistake and stand
}
give_card(&player);
if (player.score > 21) {
game_state = BJ_BUST;
}
display_player_hand();
display_score(player.score, WATCH_POSITION_SECONDS);
}
static void dealer_performs_hits(void) {
give_card(&dealer);
display_dealer_hand();
if (dealer.score > 21) {
display_win();
} else if (dealer.score > player.score) {
display_lose();
} else {
display_dealer_hand();
display_score(dealer.score, WATCH_POSITION_TOP_RIGHT);
}
}
static void see_if_dealer_hits(void) {
if (dealer.score > 16) {
if (dealer.score > player.score) {
display_lose();
} else if (dealer.score < player.score) {
display_win();
} else {
display_tie();
}
} else {
dealer_performs_hits();
}
}
static void handle_button_presses(bool tap_control_on, bool hit) {
switch (game_state)
{
case BJ_TITLE_SCREEN:
if (!tap_turned_on && tap_control_on) {
if (movement_enable_tap_detection_if_available()) tap_turned_on = true;
}
begin_playing(tap_control_on);
break;
case BJ_PLAYING:
if (hit) {
perform_hit();
} else {
perform_stand();
}
break;
case BJ_DEALER_PLAYING:
see_if_dealer_hits();
break;
case BJ_BUST:
display_bust();
break;
case BJ_RESULT:
case BJ_WIN_RATIO:
display_title();
break;
}
}
static void toggle_tap_control(blackjack_face_state_t *state) {
if (state->tap_control_on) {
movement_disable_tap_detection_if_available();
state->tap_control_on = false;
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
} else {
bool tap_could_enable = movement_enable_tap_detection_if_available();
if (tap_could_enable) {
state->tap_control_on = true;
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
}
}
}
void blackjack_face_setup(uint8_t watch_face_index, void **context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(blackjack_face_state_t));
memset(*context_ptr, 0, sizeof(blackjack_face_state_t));
blackjack_face_state_t *state = (blackjack_face_state_t *)*context_ptr;
state->tap_control_on = false;
}
g_state = (blackjack_face_state_t *)*context_ptr;
}
void blackjack_face_activate(void *context) {
blackjack_face_state_t *state = (blackjack_face_state_t *) context;
(void) state;
display_title();
stack_deck();
}
bool blackjack_face_loop(movement_event_t event, void *context) {
blackjack_face_state_t *state = (blackjack_face_state_t *) context;
switch (event.event_type) {
case EVENT_ACTIVATE:
if (state->tap_control_on) watch_set_indicator(WATCH_INDICATOR_SIGNAL);
break;
case EVENT_TICK:
if (game_state == BJ_DEALER_PLAYING) {
see_if_dealer_hits();
}
else if (game_state == BJ_BUST) {
display_bust();
}
break;
case EVENT_LIGHT_BUTTON_UP:
case EVENT_DOUBLE_TAP:
handle_button_presses(state->tap_control_on, false);
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_ALARM_BUTTON_UP:
case EVENT_SINGLE_TAP:
handle_button_presses(state->tap_control_on, true);
break;
case EVENT_LIGHT_LONG_PRESS:
if (game_state == BJ_TITLE_SCREEN) {
display_win_ratio(state);
} else {
movement_illuminate_led();
}
break;
case EVENT_ALARM_LONG_PRESS:
if (game_state == BJ_TITLE_SCREEN) {
toggle_tap_control(state);
} 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();
}
}
+96
View File
@@ -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_
+17 -81
View File
@@ -23,80 +23,6 @@
* THE USE OR OTHER DEALINGS IN THE SOFTWARE. * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
/*
* # Deadline Face
*
* This is a watch face for tracking deadlines. It draws inspiration from
* other watch faces of the project but focuses on keeping track of
* deadlines. You can enter and monitor up to four different deadlines by
* providing their respective date and time. The face has two modes:
* *running mode* and *settings mode*.
*
* ## Running Mode
*
* When the watch face is activated, it defaults to running mode. The top
* right corner shows the current deadline number, and the main display
* presents the time left until the deadline. The format of the display
* varies depending on the remaining time.
*
* - When less than a day is left, the display shows the remaining hours,
* minutes, and seconds in the form `HH:MM:SS`.
*
* - When less than a month is left, the display shows the remaining days
* and hours in the form `DD:HH` with the unit `dy` for days.
*
* - When less than a year is left, the display shows the remaining months
* and days in the form `MM:DD` with the unit `mo` for months.
*
* - When more than a year is left, the years and months are displayed in
* the form `YY:MM` with the unit `yr` for years.
*
* - When a deadline has passed in the last 24 hours, the display shows
* `over` to indicate that the deadline has just recently been reached.
*
* - When no deadline is set for a particular slot, or if a deadline has
* already passed by more than 24 hours, `--:--` is displayed.
*
* The user can navigate in running mode using the following buttons:
*
* - The *alarm button* moves the next deadline. There are currently four
* slots available for deadlines. When the last slot has been reached,
* pressing the button moves to the first slot.
*
* - A *long press* on the *alarm button* activates settings mode and
* enables configuring the currently selected deadline.
*
* - A *long press* on the *light button* activates a deadline alarm. The
* bell icon is displayed, and the alarm will ring upon reaching any of
* the deadlines set. It is important to note that the watch will not
* enter low-energy sleep mode while the alarm is enabled.
*
*
* ## Settings Mode
*
* In settings mode, the currently selected slot for a deadline can be
* configured by providing the date and the time. Like running mode, the
* top right corner of the display indicates the current deadline number.
* The main display shows the date and, on the next page, the time to be
* configured.
*
* The user can use the following buttons in settings mode.
*
* - The *light button* navigates through the different date and time
* settings, going from year, month, day, hour, to minute. The selected
* position is blinking.
*
* - A *long press* on the light button resets the date and time to the next
* day at midnight. This is the default deadline.
*
* - The *alarm button* increments the currently selected position. A *long
* press* on the *alarm button* changes the value faster.
*
* - The *mode button* exists setting mode and returns to *running mode*.
* Here the selected deadline slot can be changed.
*
*/
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "deadline_face.h" #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 */ /* Play beep sound based on type */
static inline void _beep(beep_type_t beep_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()) if (!movement_button_should_sound())
return; return;
switch (beep_type) { switch (beep_type) {
case BEEP_BUTTON: case BEEP_BUTTON:
watch_buzzer_play_note(BUZZER_NOTE_C7, 50); beep_sequence[0] = BUZZER_NOTE_C7;
beep_sequence[2] = 0;
break; break;
case BEEP_ENABLE: case BEEP_ENABLE:
watch_buzzer_play_note(BUZZER_NOTE_G7, 50); beep_sequence[0] = BUZZER_NOTE_G7;
watch_buzzer_play_note(BUZZER_NOTE_REST, 75); beep_sequence[2] = BUZZER_NOTE_REST;
watch_buzzer_play_note(BUZZER_NOTE_C8, 75); beep_sequence[4] = BUZZER_NOTE_C8;
break; break;
case BEEP_DISABLE: case BEEP_DISABLE:
watch_buzzer_play_note(BUZZER_NOTE_C8, 50); beep_sequence[0] = BUZZER_NOTE_C8;
watch_buzzer_play_note(BUZZER_NOTE_REST, 75); beep_sequence[2] = BUZZER_NOTE_REST;
watch_buzzer_play_note(BUZZER_NOTE_G7, 75); beep_sequence[4] = BUZZER_NOTE_G7;
break; break;
} }
movement_play_sequence(beep_sequence, 0);
} }
/* Change tick frequency */ /* Change tick frequency */
+74
View File
@@ -26,6 +26,80 @@
#ifndef DEADLINE_FACE_H_ #ifndef DEADLINE_FACE_H_
#define 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" #include "movement.h"
/* Modes of face */ /* Modes of face */
@@ -25,6 +25,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "endless_runner_face.h" #include "endless_runner_face.h"
#include "delay.h"
typedef enum { typedef enum {
JUMPING_FINAL_FRAME = 0, JUMPING_FINAL_FRAME = 0,
@@ -34,6 +35,7 @@ typedef enum {
typedef enum { typedef enum {
SCREEN_TITLE = 0, SCREEN_TITLE = 0,
SCREEN_SCORE,
SCREEN_PLAYING, SCREEN_PLAYING,
SCREEN_LOSE, SCREEN_LOSE,
SCREEN_TIME, SCREEN_TIME,
@@ -77,14 +79,45 @@ typedef struct {
uint8_t fuel; uint8_t fuel;
} game_state_t; } 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 game_state_t game_state;
static const uint8_t _num_bits_obst_pattern = sizeof(game_state.obst_pattern) * 8; 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) { static void print_binary(uint32_t value, int bits) {
#if __EMSCRIPTEN__ #if __EMSCRIPTEN__
for (int i = bits - 1; i >= 0; i--) { for (int i = bits - 1; i >= 0; i--) {
// Print each bit // Print each bit
printf("%lu", (value >> i) & 1); printf("%u", (value >> i) & 1);
// Optional: add a space every 4 bits for readability // Optional: add a space every 4 bits for readability
if (i % 4 == 0 && i != 0) { if (i % 4 == 0 && i != 0) {
printf(" "); printf(" ");
@@ -188,22 +221,22 @@ static uint32_t get_random_legal(uint32_t prev_val, uint16_t difficulty) {
static void display_ball(bool jumping) { static void display_ball(bool jumping) {
if (!jumping) { if (!jumping) {
watch_set_pixel(0, 21); watch_set_pixel(ball_arr_com[3], ball_arr_seg[3]);
watch_set_pixel(1, 21); watch_set_pixel(ball_arr_com[2], ball_arr_seg[2]);
watch_set_pixel(0, 20); watch_set_pixel(ball_arr_com[1], ball_arr_seg[1]);
watch_set_pixel(1, 20); watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]);
watch_clear_pixel(1, 17); watch_clear_pixel(ball_arr_com[6], ball_arr_seg[6]);
watch_clear_pixel(2, 20); watch_clear_pixel(ball_arr_com[5], ball_arr_seg[5]);
watch_clear_pixel(2, 21); watch_clear_pixel(ball_arr_com[4], ball_arr_seg[4]);
} }
else { else {
watch_clear_pixel(0, 21); watch_clear_pixel(ball_arr_com[3], ball_arr_seg[3]);
watch_clear_pixel(1, 21); watch_clear_pixel(ball_arr_com[2], ball_arr_seg[2]);
watch_clear_pixel(0, 20); watch_clear_pixel(ball_arr_com[1], ball_arr_seg[1]);
watch_set_pixel(1, 20); watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]);
watch_set_pixel(1, 17); watch_set_pixel(ball_arr_com[6], ball_arr_seg[6]);
watch_set_pixel(2, 20); watch_set_pixel(ball_arr_com[5], ball_arr_seg[5]);
watch_set_pixel(2, 21); 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) { if (game_state.fuel_mode) {
score %= (MAX_DISP_SCORE_FUEL + 1); score %= (MAX_DISP_SCORE_FUEL + 1);
sprintf(buf, "%1d", score); sprintf(buf, "%1d", score);
watch_display_string(buf, 0); watch_display_text(WATCH_POSITION_TOP_LEFT, buf);
} }
else { else {
score %= (MAX_DISP_SCORE + 1); score %= (MAX_DISP_SCORE + 1);
sprintf(buf, "%2d", score); 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) { static void display_fuel(uint8_t subsecond, uint8_t difficulty) {
char buf[4]; char buf[4];
if (difficulty == DIFF_FUEL_1 && game_state.fuel == 0 && subsecond % (FREQ/2) == 0) { 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; return;
} }
sprintf(buf, "%2d", game_state.fuel); 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) { static void check_and_reset_hi_score(endless_runner_state_t *state) {
// Resets the hi score at the beginning of each month. // 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) || if ((state -> year_last_hi_score != date_time.unit.year) ||
(state -> month_last_hi_score != date_time.unit.month)) (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) { static void display_difficulty(uint16_t difficulty) {
switch (difficulty) static const char *labels[] = {
{ [DIFF_BABY] = " b",
case DIFF_BABY: [DIFF_EASY] = " E",
watch_display_string(" b", 2); [DIFF_HARD] = " H",
break; [DIFF_FUEL] = " F",
case DIFF_EASY: [DIFF_FUEL_1] = "1F",
watch_display_string(" E", 2); [DIFF_NORM] = " N"
break; };
case DIFF_HARD: watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]);
watch_display_string(" H", 2);
break;
case DIFF_FUEL:
watch_display_string(" F", 2);
break;
case DIFF_FUEL_1:
watch_display_string("1F", 2);
break;
case DIFF_NORM:
default:
watch_display_string(" N", 2);
break;
}
game_state.fuel_mode = difficulty >= DIFF_FUEL && difficulty <= DIFF_FUEL_1; 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) { static void display_sound_indicator(bool soundOn) {
state -> soundOn = !state -> soundOn; if (soundOn){
if (state -> soundOn){
watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
watch_set_indicator(WATCH_INDICATOR_BELL); watch_set_indicator(WATCH_INDICATOR_BELL);
} } else {
else {
watch_clear_indicator(WATCH_INDICATOR_BELL); 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) { 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; uint16_t hi_score = state -> hi_score;
uint8_t difficulty = state -> difficulty; uint8_t difficulty = state -> difficulty;
bool sound_on = state -> soundOn; bool sound_on = state -> soundOn;
game_state.curr_screen = SCREEN_TITLE;
memset(&game_state, 0, sizeof(game_state)); 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. 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 if (sound_on) game_state.sec_before_moves--; // Start chime is about 1 second
watch_set_colon(); watch_set_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN ", "ER ");
if (hi_score > MAX_HI_SCORE) { if (hi_score > MAX_HI_SCORE) {
watch_display_string("ER HS --", 0); watch_display_text(WATCH_POSITION_BOTTOM, "HS --");
} }
else { else {
char buf[14]; char buf[10];
sprintf(buf, "ER HS%4d", hi_score); sprintf(buf, "HS%4d", hi_score);
watch_display_string(buf, 0); watch_display_text(WATCH_POSITION_BOTTOM, buf);
} }
display_difficulty(difficulty); 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; 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]; char buf[6 + 1];
// If the hour needs updating or it's the first time displaying the time // 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)) { if ((game_state.curr_screen != SCREEN_TIME) || (date_time.unit.hour != previous_date_time.unit.hour)) {
uint8_t hour = date_time.unit.hour; uint8_t hour = date_time.unit.hour;
game_state.curr_screen = SCREEN_TIME; game_state.curr_screen = SCREEN_TIME;
if (!watch_sleep_animation_is_running()) {
if (clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); 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 { else {
if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM); if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM);
hour %= 12; hour %= 12;
if (hour == 0) hour = 12; if (hour == 0) hour = 12;
} }
watch_set_colon(); sprintf( buf, clock_mode_24h == MOVEMENT_CLOCK_MODE_024H ? "%02d%02d " : "%2d%02d ", hour, date_time.unit.minute);
sprintf( buf, "%2d%02d ", hour, date_time.unit.minute); watch_display_text(WATCH_POSITION_BOTTOM, buf);
watch_display_string(buf, 4);
} }
// If both digits of the minute need updating // If only the minute need updating
else if ((date_time.unit.minute / 10) != (previous_date_time.unit.minute / 10)) { else {
sprintf( buf, "%02d ", date_time.unit.minute); sprintf( buf, "%02d", date_time.unit.minute);
watch_display_string(buf, 6); watch_display_text(WATCH_POSITION_MINUTES, buf);
}
// If only the ones-place of the minute needs updating.
else if (date_time.unit.minute != previous_date_time.unit.minute) {
sprintf( buf, "%d ", date_time.unit.minute % 10);
watch_display_string(buf, 7);
} }
previous_date_time.reg = date_time.reg; 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; uint8_t difficulty = state -> difficulty;
game_state.curr_screen = SCREEN_PLAYING; game_state.curr_screen = SCREEN_PLAYING;
watch_clear_colon(); watch_clear_colon();
display_sound_indicator(state -> soundOn);
movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ); movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ);
if (game_state.fuel_mode) { if (game_state.fuel_mode) {
watch_display_string(" ", 0); watch_clear_display();
game_state.obst_pattern = get_random_fuel(0); game_state.obst_pattern = get_random_fuel(0);
if ((16 * JUMP_FRAMES_FUEL_RECHARGE) < JUMP_FRAMES_FUEL) // 16 frames of zeros at the start of a level 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. 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; if (game_state.fuel < JUMP_FRAMES_FUEL_RECHARGE) game_state.fuel = JUMP_FRAMES_FUEL_RECHARGE;
} }
else { 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.obst_pattern = get_random_legal(0, difficulty);
} }
game_state.jump_state = NOT_JUMPING; game_state.jump_state = NOT_JUMPING;
display_ball(game_state.jump_state != NOT_JUMPING); display_ball(game_state.jump_state != NOT_JUMPING);
display_score( game_state.curr_score); display_score( game_state.curr_score);
if (state -> soundOn){ if (state -> soundOn){
watch_buzzer_play_note(BUZZER_NOTE_C5, 200); watch_buzzer_play_sequence(start_tune, NULL);
watch_buzzer_play_note(BUZZER_NOTE_E5, 200);
watch_buzzer_play_note(BUZZER_NOTE_G5, 200);
} }
} }
static void display_lose_screen(endless_runner_state_t *state) { static void display_lose_screen(endless_runner_state_t *state) {
game_state.curr_screen = SCREEN_LOSE; game_state.curr_screen = SCREEN_LOSE;
game_state.curr_score = 0; game_state.curr_score = 0;
watch_display_string(" LOSE ", 0); watch_clear_display();
if (state -> soundOn) watch_display_text(WATCH_POSITION_BOTTOM, " LOSE ");
watch_buzzer_play_note(BUZZER_NOTE_A1, 600); if (state -> soundOn) {
else watch_buzzer_play_sequence(lose_tune, NULL);
delay_ms(600); delay_ms(600);
}
} }
static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t *state) { 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: case 2:
game_state.loc_2_on = obstacle; game_state.loc_2_on = obstacle;
if (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) { 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) if (game_state.fuel_mode && prev_obst_pos_two)
add_to_score(state); add_to_score(state);
} }
@@ -406,55 +455,20 @@ static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t
case 3: case 3:
game_state.loc_3_on = obstacle; game_state.loc_3_on = obstacle;
if (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) 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; break;
case 1: case 1:
if (!game_state.fuel_mode && obstacle) // If an obstacle is here, it means the ball cleared it if (!game_state.fuel_mode && obstacle) // If an obstacle is here, it means the ball cleared it
add_to_score(state); add_to_score(state);
//fall through //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: 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; 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)); memset(*context_ptr, 0, sizeof(endless_runner_state_t));
endless_runner_state_t *state = (endless_runner_state_t *)*context_ptr; endless_runner_state_t *state = (endless_runner_state_t *)*context_ptr;
state->difficulty = DIFF_NORM; state->difficulty = DIFF_NORM;
state->tap_control_on = false;
} }
} }
void endless_runner_face_activate(void *context) { void endless_runner_face_activate(void *context) {
(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) { bool endless_runner_face_loop(movement_event_t event, void *context) {
endless_runner_state_t *state = (endless_runner_state_t *)context; endless_runner_state_t *state = (endless_runner_state_t *)context;
switch (event.event_type) { switch (event.event_type) {
case EVENT_ACTIVATE: case EVENT_ACTIVATE:
disable_tap_control(state);
check_and_reset_hi_score(state); check_and_reset_hi_score(state);
if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL);
display_title(state); display_title(state);
break; break;
case EVENT_TICK: case EVENT_TICK:
switch (game_state.curr_screen) switch (game_state.curr_screen)
{ {
case SCREEN_TITLE: case SCREEN_TITLE:
case SCREEN_SCORE:
case SCREEN_LOSE: case SCREEN_LOSE:
case SCREEN_TIME:
break; break;
default: default:
update_game(state, event.subsecond); update_game(state, event.subsecond);
@@ -574,15 +599,37 @@ bool endless_runner_face_loop(movement_event_t event, void *context) {
break; break;
case EVENT_LIGHT_BUTTON_UP: case EVENT_LIGHT_BUTTON_UP:
case EVENT_ALARM_BUTTON_UP: case EVENT_ALARM_BUTTON_UP:
if (game_state.curr_screen == SCREEN_TITLE) switch (game_state.curr_screen) {
begin_playing(state); case SCREEN_SCORE:
else if (game_state.curr_screen == SCREEN_LOSE) enable_tap_control(state);
display_title(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; break;
case EVENT_LIGHT_LONG_PRESS: case EVENT_LIGHT_LONG_PRESS:
if (game_state.curr_screen == SCREEN_TITLE) if (game_state.curr_screen == SCREEN_SCORE)
change_difficulty(state); change_difficulty(state);
break; 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_LIGHT_BUTTON_DOWN:
case EVENT_ALARM_BUTTON_DOWN: case EVENT_ALARM_BUTTON_DOWN:
if (game_state.curr_screen == SCREEN_PLAYING && game_state.jump_state == NOT_JUMPING){ 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; break;
case EVENT_ALARM_LONG_PRESS: 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); toggle_sound(state);
break; break;
case EVENT_TIMEOUT: case EVENT_TIMEOUT:
if (game_state.curr_screen != SCREEN_TITLE) disable_tap_control(state);
display_title(state); if (game_state.curr_screen != SCREEN_SCORE)
display_score_screen(state);
break; break;
case EVENT_LOW_ENERGY_UPDATE: 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; break;
default: default:
return movement_default_loop_handler(event); 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 endless_runner_face_resign(void *context) {
(void) context; endless_runner_state_t *state = (endless_runner_state_t *)context;
disable_tap_control(state);
} }
@@ -33,6 +33,8 @@
This is a basic endless-runner, like the [Chrome Dino game](https://en.wikipedia.org/wiki/Dinosaur_Game). 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. 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. 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. 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 month_last_hi_score : 4;
uint8_t year_last_hi_score : 6; uint8_t year_last_hi_score : 6;
uint8_t soundOn : 1; 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; } endless_runner_state_t;
void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr); void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr);
+244 -227
View File
@@ -2,6 +2,7 @@
* MIT License * MIT License
* *
* Copyright (c) 2022 Andreas Nebinger * Copyright (c) 2022 Andreas Nebinger
* Copyright (c) 2025 Alessandro Genova
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -24,11 +25,13 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <limits.h>
#include "fast_stopwatch_face.h" #include "fast_stopwatch_face.h"
#include "watch.h" #include "watch.h"
#include "watch_common_display.h" #include "watch_common_display.h"
#include "watch_utility.h" #include "watch_utility.h"
#include "watch_rtc.h" #include "watch_rtc.h"
#include "slcd.h"
/* /*
This watch face implements the original F-91W stopwatch functionality This watch face implements the original F-91W stopwatch functionality
@@ -40,173 +43,247 @@
turns on on each button press or it doesn't. turns on on each button press or it doesn't.
*/ */
#if __EMSCRIPTEN__ // Loosely implement the watch as a state machine
#include <emscripten.h> typedef enum {
#include <emscripten/html5.h> SW_STATUS_IDLE = 0,
#else SW_STATUS_RUNNING,
#include "tc.h" SW_STATUS_RUNNING_LAPPING,
#endif SW_STATUS_STOPPED,
SW_STATUS_STOPPED_LAPPING
// distant future for background task: January 1, 2083 } stopwatch_status_t;
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
static inline void _button_beep() { static inline void _button_beep() {
// play a beep as confirmation for a button press (if applicable) // 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()); 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 /// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter
/// on the lcd. /// on the lcd.
/// @param ticks /// @param ticks
static void _display_ticks(uint32_t ticks) { static void _display_elapsed(fast_stopwatch_state_t *state, uint32_t ticks) {
char buf[14]; char buf[3];
uint8_t sec_100 = (ticks & 0x7F) * 100 / 128;
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; 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; 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); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
} else { } else {
watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); 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_indicators(fast_stopwatch_state_t *state, movement_event_t event, uint32_t elapsed) {
static void _draw() { uint8_t subsecond;
if (_lap_ticks == 0) { bool tock;
char buf[14];
uint8_t sec_100 = (_ticks & 0x7F) * 100 / 128; switch (state->status) {
if (_is_running) { case SW_STATUS_RUNNING:
uint32_t seconds = _ticks >> 7; subsecond = elapsed & 127;
if (seconds != _old_seconds) { tock = subsecond >= 64;
// seconds have changed
_old_seconds = seconds; watch_clear_indicator(WATCH_INDICATOR_LAP);
uint8_t minutes = seconds / 60; if (tock) {
seconds %= 60; watch_clear_colon();
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);
}
} else { } else {
// only draw 100ths of seconds watch_set_colon();
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
} }
} else {
_display_ticks(_ticks); return;
}
} case SW_STATUS_RUNNING_LAPPING:
if (_is_running) { tock = event.subsecond > 0;
// blink the colon every half second
uint8_t blink_ticks = ((_ticks >> 6) & 1); if (tock) {
if (blink_ticks != _blink_ticks) { watch_clear_indicator(WATCH_INDICATOR_LAP);
_blink_ticks = blink_ticks; watch_clear_colon();
_colon = !_colon; } else {
if (_colon) watch_set_colon(); watch_set_indicator(WATCH_INDICATOR_LAP);
else watch_clear_colon(); 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() { static uint8_t get_refresh_rate(fast_stopwatch_state_t *state) {
if (_lap_ticks) watch_set_indicator(WATCH_INDICATOR_LAP); switch (state->status) {
else watch_clear_indicator(WATCH_INDICATOR_LAP); 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() { static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counter, movement_event_type_t event_type) {
watch_set_colon(); switch (state->status) {
_colon = true; 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) { 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)); *context_ptr = malloc(sizeof(fast_stopwatch_state_t));
memset(*context_ptr, 0, 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; fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)*context_ptr;
_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; state->start_counter = 0;
_is_running = _colon = false; state->stop_counter = 0;
state->light_on_button = true; state->lap_counter = 0;
} state->status = SW_STATUS_IDLE;
if (!_is_running) {
// prepare the 128 Hz callback source
_cb_initialize();
} }
} }
void fast_stopwatch_face_activate(void *context) { void fast_stopwatch_face_activate(void *context) {
(void) context; fast_stopwatch_state_t *state = (fast_stopwatch_state_t *) context;
if (_is_running) { // force full re-draw
// The background task will keep the watch from entering low energy mode while the stopwatch is on screen. state->old_display.seconds = UINT_MAX;
movement_schedule_background_task(distant_future); 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) { bool fast_stopwatch_face_loop(movement_event_t event, void *context) {
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)context; fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)context;
// handle overflow of fast ticks rtc_counter_t counter = watch_rtc_get_counter();
while (_ticks >= (128 * 60 * 60)) {
_ticks -= (128 * 60 * 60); state_transition(state, counter, event.event_type);
_hours++; rtc_counter_t elapsed = elapsed_time(state, counter);
if (_hours >= 24) _hours -= 24;
// initiate a re-draw
_old_minutes = 59;
}
switch (event.event_type) { switch (event.event_type) {
case EVENT_ACTIVATE: case EVENT_ACTIVATE:
_set_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST"); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST");
_update_lap_indicator(); _draw_indicators(state, event, elapsed);
if (_is_running) movement_request_tick_frequency(16); _display_elapsed(state, elapsed);
_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();
break; break;
case EVENT_ALARM_BUTTON_DOWN: 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: case EVENT_LIGHT_BUTTON_DOWN:
if (state->light_on_button) movement_illuminate_led(); case EVENT_LIGHT_LONG_PRESS:
if (_is_running) { _button_beep();
if (_lap_ticks) { // fall through
// clear lap and continue running case EVENT_TICK:
_lap_ticks = 0; _draw_indicators(state, event, elapsed);
movement_request_tick_frequency(16); _display_elapsed(state, elapsed);
} 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();
break; break;
default: default:
movement_default_loop_handler(event); movement_default_loop_handler(event);
break; break;
} }
return true; return true;
} }
void fast_stopwatch_face_resign(void *context) { void fast_stopwatch_face_resign(void *context) {
(void) context; (void) context;
// cancel the keepalive task movement_request_tick_frequency(1);
movement_cancel_background_task();
} }
+10 -7
View File
@@ -55,7 +55,16 @@
#include "movement.h" #include "movement.h"
typedef struct { 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; } fast_stopwatch_state_t;
void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr); 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); bool fast_stopwatch_face_loop(movement_event_t event, void *context);
void fast_stopwatch_face_resign(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){ \ #define fast_stopwatch_face ((const watch_face_t){ \
fast_stopwatch_face_setup, \ fast_stopwatch_face_setup, \
fast_stopwatch_face_activate, \ fast_stopwatch_face_activate, \
@@ -30,19 +30,22 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "higher_lower_game_face.h" #include "higher_lower_game_face.h"
#include "watch_private_display.h" #include "watch_common_display.h"
#define KING 12
#define QUEEN 11
#define JACK 10
#define TITLE_TEXT "Hi-Lo" #define TITLE_TEXT "Hi-Lo"
#define GAME_BOARD_SIZE 6 #define GAME_BOARD_SIZE 6
#define MAX_BOARDS 40 #define MAX_BOARDS 40
#define GUESSES_PER_SCREEN 5 #define GUESSES_PER_SCREEN 5
#define WIN_SCORE (MAX_BOARDS * GUESSES_PER_SCREEN) #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_START 4
#define BOARD_DISPLAY_END 9 #define BOARD_DISPLAY_END 9
#define MIN_CARD_VALUE 2 #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_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1)
#define CARD_SUIT_COUNT 4 #define CARD_SUIT_COUNT 4
#define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT) #define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT)
@@ -111,7 +114,6 @@ static void shuffle_deck(void) {
static void reset_deck(void) { static void reset_deck(void) {
current_card = 0; current_card = 0;
stack_deck();
shuffle_deck(); shuffle_deck();
} }
@@ -141,8 +143,8 @@ static void reset_board(bool first_round) {
static void init_game(void) { static void init_game(void) {
watch_clear_display(); watch_clear_display();
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START); watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
watch_display_string("GA", STATUS_DISPLAY_START); watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
reset_deck(); reset_deck();
reset_board(true); reset_board(true);
score = 0; score = 0;
@@ -151,16 +153,23 @@ static void init_game(void) {
} }
static void set_segment_at_position(segment_t segment, uint8_t position) { 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; digit_mapping_t segmap;
const uint8_t com_pin = position_segment_data >> 6; if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) {
const uint8_t seg = position_segment_data & 0x3F; 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); 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) { static void render_board_position(size_t board_position) {
const size_t display_position = FLIP_BOARD_DIRECTION const size_t display_position = get_display_position(board_position);
? BOARD_DISPLAY_START + board_position
: BOARD_DISPLAY_END - board_position;
const bool revealed = game_board[board_position].revealed; const bool revealed = game_board[board_position].revealed;
//// Current position indicator spot //// 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; const uint8_t value = game_board[board_position].value;
switch (value) { switch (value) {
case 14: // A (≡) case KING: // K (≡)
watch_display_character(' ', display_position); watch_display_character(' ', display_position);
set_segment_at_position(A, display_position); set_segment_at_position(A, display_position);
set_segment_at_position(D, display_position); set_segment_at_position(D, display_position);
set_segment_at_position(G, display_position); set_segment_at_position(G, display_position);
break; break;
case 13: // K (=) case QUEEN: // Q (=)
watch_display_character(' ', display_position); watch_display_character(' ', display_position);
set_segment_at_position(A, display_position); set_segment_at_position(A, display_position);
set_segment_at_position(D, display_position); set_segment_at_position(D, display_position);
break; break;
case 12: // Q (-) case JACK: // J (-)
watch_display_character('-', display_position); watch_display_character('-', display_position);
break; break;
default: { default: {
@@ -209,16 +218,16 @@ static void render_board_count(void) {
// Render completed boards (screens) // Render completed boards (screens)
char buf[3] = {0}; char buf[3] = {0};
snprintf(buf, sizeof(buf), "%2hhu", completed_board_count); 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) { 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}; char buf[7] = {0};
const uint8_t complete_boards = score / GUESSES_PER_SCREEN; const uint8_t complete_boards = score / GUESSES_PER_SCREEN;
snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score); snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score);
watch_set_colon(); watch_set_colon();
watch_display_string(buf, BOARD_DISPLAY_START); watch_display_text(WATCH_POSITION_BOTTOM, buf);
} }
static guess_t get_answer(void) { static guess_t get_answer(void) {
@@ -251,13 +260,13 @@ static void do_game_loop(guess_t user_guess) {
// Render answer indicator // Render answer indicator
switch (answer) { switch (answer) {
case HL_GUESS_EQUAL: case HL_GUESS_EQUAL:
watch_display_string("==", STATUS_DISPLAY_START); watch_display_text(WATCH_POSITION_TOP_LEFT, "==");
break; break;
case HL_GUESS_HIGHER: case HL_GUESS_HIGHER:
watch_display_string("HI", STATUS_DISPLAY_START); watch_display_text(WATCH_POSITION_TOP_LEFT, "HI");
break; break;
case HL_GUESS_LOWER: case HL_GUESS_LOWER:
watch_display_string("LO", STATUS_DISPLAY_START); watch_display_text(WATCH_POSITION_TOP_LEFT, "LO");
break; break;
} }
@@ -268,18 +277,22 @@ static void do_game_loop(guess_t user_guess) {
// No score for two consecutive identical cards // No score for two consecutive identical cards
} else { } else {
// Incorrect guess, game over // 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; game_board[guess_position].revealed = true;
watch_display_text(WATCH_POSITION_BOTTOM, "------");
render_board_position(guess_position - 1);
render_board_position(guess_position); 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; game_state = HL_GS_LOSE;
return; return;
} }
if (score >= WIN_SCORE) { if (score >= WIN_SCORE) {
// Win, perhaps some kind of animation sequence? // Win, perhaps some kind of animation sequence?
watch_display_string("WI", STATUS_DISPLAY_START); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "WI");
watch_display_string(" ", BOARD_SCORE_DISPLAY_START); watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_string("------", BOARD_DISPLAY_START); watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WINNER", "winnEr");
game_state = HL_GS_WIN; game_state = HL_GS_WIN;
return; return;
} }
@@ -309,12 +322,12 @@ static void do_game_loop(guess_t user_guess) {
break; break;
case HL_GS_SHOW_SCORE: case HL_GS_SHOW_SCORE:
watch_clear_display(); watch_clear_display();
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START); watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
watch_display_string("GA", STATUS_DISPLAY_START); watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
game_state = HL_GS_TITLE_SCREEN; game_state = HL_GS_TITLE_SCREEN;
break; break;
default: default:
watch_display_string("ERROR", BOARD_DISPLAY_START); watch_display_text(WATCH_POSITION_BOTTOM, "ERROR");
break; break;
} }
} }
@@ -344,6 +357,7 @@ void higher_lower_game_face_activate(void *context) {
(void) state; (void) state;
// Handle any tasks related to your watch face coming on screen. // Handle any tasks related to your watch face coming on screen.
game_state = HL_GS_TITLE_SCREEN; game_state = HL_GS_TITLE_SCREEN;
stack_deck();
} }
bool higher_lower_game_face_loop(movement_event_t event, void *context) { 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) { switch (event.event_type) {
case EVENT_ACTIVATE: case EVENT_ACTIVATE:
// Show your initial UI here. // Show your initial UI here.
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START); watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
watch_display_string("GA", STATUS_DISPLAY_START); watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
break; break;
case EVENT_TICK: case EVENT_TICK:
// If needed, update your display here. // If needed, update your display here.
+1 -2
View File
@@ -96,8 +96,7 @@ static inline void _inc_uint8(uint8_t *value, uint8_t step, uint8_t max) {
static uint32_t _get_now_ts() { static uint32_t _get_now_ts() {
// returns the current date time as unix timestamp // returns the current date time as unix timestamp
watch_date_time_t now = watch_rtc_get_date_time(); return movement_get_utc_timestamp();
return watch_utility_date_time_to_unix_time(now, 0);
} }
static inline void _button_beep() { static inline void _button_beep() {
+577
View File
@@ -0,0 +1,577 @@
/*
* MIT License
*
* Copyright (c) 2024 Klingon Jane
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Emulator only: need time() to seed the random number generator.
#if __EMSCRIPTEN__
#include <time.h>
#endif
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "lander_face.h"
#include "watch_common_display.h"
#ifndef max
#define max(x, y) ((y) > (x) ? (y) : (x))
#endif
#ifndef min
#define min(x, y) ((x) > (y) ? (y) : (x))
#endif
#define LANDER_TICK_FREQUENCY 8
#define MONSTER_DISPLAY_TICKS 9
#define ENGINE_THRUST 11
#define MODE_WAITING_TO_START 0
#define MODE_DISPLAY_SKILL_LEVEL 1
#define MODE_PLAYING 2
#define MODE_TOUCHDOWN_BLANK 3
#define MODE_DISPLAY_FINAL_STATUS 4
#define MODE_MONSTER 5
#define MODE_FIND_EARTH_MESSAGE 6
#define CREWS_COMPLIMENT 13
// Granularity is divisions per foot - height display
#define GRANUL 40
// Next lines for repeat heroes only.
#define PROMOTION_INTERVAL 3
#define LEVEL_ACE 8
#define LEVEL_STARBUCK 11
#define HARD_EARTH_INCREMENTS 11
#define MAX_HARD_EARTH_CHANCE 6
// The gory final result calculations:
#define SPEED_FATALITY_ALL 41
#define SPEED_FATALITY_NONE 26
#define SPEED_NO_DAMAGE 21
#define SPEED_LEVEL_INCREMENTS 2
#define SPEED_MAJOR_CRASH 73
#define MAJOR_CRASH_INCREMENTS 65
#define SPEED_INJURY_NONE 20
#define SPEED_INJURY_FULCRUM 32
#define INJURY_FULCRUM_PROB 65
#define FUEL_SCORE_GOOD 145
#define FUEL_SCORE_GREAT 131
#define FUEL_SCORE_FANTASTIC 125
// Joey Castillo to oversee storage allocation row
#define LANDER_STORAGE_ROW 2
#define STORAGE_KEY_NUMBER 110
#define DIFFICULTY_LEVELS 3
char lander_difficulty_names[DIFFICULTY_LEVELS][7] = {
"NOrMAL", "HArd ", "HArdEr"
};
#define MONSTER_TYPES 4
char lander_monster_names[MONSTER_TYPES][7] = {
"mOnStr", "6Erbil", "HAmStr", "Rabbit"
};
#define MONSTER_ACTIONS 8
char lander_monster_actions[MONSTER_ACTIONS][7] = {
"HUn6ry", " EAtS", "6Reedy", "annoYd", "nASty ", "SAVOry", "HO66SH", " pI66Y"
};
// --------------
// Custom methods
// --------------
static int gen_random_int (int16_t lower, int16_t upper) {
int range;
int retVal;
range = upper - lower + 1;
if ( range < 2 ) range = 2;
// Emulator: use rand. Hardware: use arc4random.
#if __EMSCRIPTEN__
retVal = rand() % range;
#else
retVal = arc4random_uniform(range);
#endif
retVal += lower;
return retVal;
}
static uint8_t assignProb ( uint8_t lowerProb, uint8_t upperProb, int16_t lowerSpeed, int16_t upperSpeed, int16_t actSpeed ) {
float probRange, speedRange;
float ratio, probFloat;
int probInt;
speedRange = upperSpeed - lowerSpeed;
if (speedRange<1.0) speedRange = 1.0;
probRange = upperProb - lowerProb;
ratio = ( (float) actSpeed - (float) lowerSpeed ) / speedRange;
probFloat = (float) lowerProb + ( ratio * probRange );
probInt = (int) ( probFloat + 0.5 );
probInt = min ( probInt, upperProb );
probInt = max ( probInt, lowerProb );
return (uint8_t) probInt;
}
static void write_to_lander_EEPROM(lander_state_t *state) {
uint8_t output_array [ 3 ];
output_array [ 0 ] = STORAGE_KEY_NUMBER;
output_array [ 1 ] = state->hero_counter;
output_array [ 2 ] = state->legend_counter;
watch_storage_erase ( LANDER_STORAGE_ROW );
watch_storage_sync ( );
watch_storage_write ( LANDER_STORAGE_ROW, 0, output_array, 3 );
}
// ---------------------------
// Standard watch face methods
// ---------------------------
void lander_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(lander_state_t));
memset(*context_ptr, 0, sizeof(lander_state_t));
lander_state_t *state = (lander_state_t *)*context_ptr;
state->led_enabled = false;
}
// Emulator only: Seed random number generator
#if __EMSCRIPTEN__
srand(time(NULL));
#endif
}
void lander_face_activate(void *context) {
lander_state_t *state = (lander_state_t *)context;
char buf [ 7 ];
state->mode = MODE_WAITING_TO_START;
state->led_active = false;
state->reset_counter = 0;
watch_clear_all_indicators ( );
uint32_t offset = 0;
uint32_t size = 3;
uint8_t stored_data [ size ];
// See if the hero_counter was ever written to EEPROM storage
watch_storage_read (LANDER_STORAGE_ROW, offset, stored_data, size);
if (stored_data[0] == STORAGE_KEY_NUMBER )
{
state->hero_counter = stored_data [1]; // There's real data in there.
state->legend_counter = stored_data [2];
}
else
{
state->hero_counter = 0; // Nope. Nothing there.
state->legend_counter = 0;
write_to_lander_EEPROM(state); // Initial EEPROM tracking data.
}
state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL;
state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit
// Fancy intro
if ( state->legend_counter == 0 ) watch_display_text(WATCH_POSITION_TOP_LEFT, "LA");
else watch_display_text(WATCH_POSITION_TOP_LEFT, "LE");
if ( ( state->hero_counter == 0 ) || ( state->hero_counter >= 40 ) ) watch_display_text ( WATCH_POSITION_TOP_RIGHT, " ");
else
{
sprintf ( buf, "%2d", state->hero_counter );
watch_display_text ( WATCH_POSITION_TOP_RIGHT, buf);
}
if ( state->hero_counter >= 100 ) sprintf ( buf, "Str%3d", state->hero_counter );
else if ( state->hero_counter >= 40 ) sprintf ( buf, "Strb%2d", state->hero_counter );
else if ( state->hero_counter >= LEVEL_STARBUCK ) sprintf ( buf, "StrbUC" );
else if ( state->hero_counter >= LEVEL_ACE ) sprintf ( buf, " ACE " ); // This human is good
else if ( state->difficulty_level == 0 ) sprintf ( buf, " " );
else sprintf ( buf, "%s", lander_difficulty_names[state->difficulty_level] );
watch_display_text ( WATCH_POSITION_BOTTOM, buf);
if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
}
bool lander_face_loop(movement_event_t event, void *context) {
lander_state_t *state = (lander_state_t *)context;
char buf [ 20 ]; // [11] is more correct and works; compiler too helpful.
switch (event.event_type) {
case EVENT_TICK:
state->tick_counter++;
if ( state->mode == MODE_PLAYING ) {
int16_t accel = state->gravity;
bool gas_pedal_on = HAL_GPIO_BTN_ALARM_read() || HAL_GPIO_BTN_LIGHT_read();
if ( gas_pedal_on && ( state->fuel_remaining > 0 ) ) {
accel = ENGINE_THRUST + state->gravity; // Gravity is negative
state->fuel_remaining--; // Used 1 fuel unit
watch_set_indicator ( WATCH_INDICATOR_LAP );
// Low fuel warning indicators
if ( state->fuel_remaining == ( 3 * LANDER_TICK_FREQUENCY ) ) { // 3 seconds of fuel left
watch_set_indicator ( WATCH_INDICATOR_SIGNAL );
watch_set_indicator ( WATCH_INDICATOR_BELL );
watch_set_indicator ( WATCH_INDICATOR_PM );
watch_set_indicator ( WATCH_INDICATOR_24H );
}
else if ( state->fuel_remaining == 0 ) { // 0 seconds of fuel left, empty!
watch_clear_all_indicators ( );
}
}
else {
watch_clear_indicator ( WATCH_INDICATOR_LAP );
}
state->speed += accel;
state->height += state->speed;
if ( state->height > 971 * 80 ) { // Escape height
watch_clear_all_indicators ();
watch_display_text( WATCH_POSITION_BOTTOM, "ESCAPE" );
state->tick_counter = 0;
state->mode = MODE_WAITING_TO_START;
}
else if ( state->height <= 0 ) { // Touchdown
state->tick_counter = 0;
state->mode = MODE_TOUCHDOWN_BLANK;
}
else {
// Update height display
sprintf ( buf, "%4d", (int) ( state->height / GRANUL ) );
watch_display_text( WATCH_POSITION_BOTTOM, buf );
}
}
else if ( state->mode == MODE_TOUCHDOWN_BLANK ) {
// Blank display on touchdown
if ( state->tick_counter == 1 ) {
watch_clear_all_indicators ();
watch_display_text( WATCH_POSITION_BOTTOM, " " );
// Also calc fuel score now.
float fuel_score_float;
uint16_t fuel_used;
fuel_used = state->fuel_start - state->fuel_remaining;
fuel_score_float = (float) fuel_used / (float) state->fuel_tpl;
state->fuel_score = (int) (fuel_score_float * 100.0 + 0.5);
if ( state->legend_counter == 0 ) state->fuel_score -= 8; // First Earth is easier
// Monitor reset_counter
if ( fuel_used >= 1 ) state->reset_counter = 0;
else state->reset_counter++;
if ( state->reset_counter >= 3 ) {
state->hero_counter = 0;
state->difficulty_level = 0;
if ( state->reset_counter >= 6 ) state->legend_counter = 0;
watch_display_text(WATCH_POSITION_BOTTOM, "rESET ");
write_to_lander_EEPROM(state);
}
}
// Wait until time for next display
if ( state->tick_counter >= ( 1 * LANDER_TICK_FREQUENCY ) ) {
state->tick_counter = 0;
state->mode = MODE_DISPLAY_FINAL_STATUS;
}
}
else if ( state->mode == MODE_DISPLAY_FINAL_STATUS ) {
bool last_pass = false;
if ( state->tick_counter >= LANDER_TICK_FREQUENCY ) last_pass = true;
// Show final status
if ( state->tick_counter == 1 ) {
// Calculate many attributes
// 1) Major crash: bug, crater, vaporized (gone).
// 2) Rank ship's health 0 to 8
// 3) Crew fatalities and injuries
// 4) Special conditions: hero
// 5) Set fuel conservation indicators as appropriate
// 6) Set coffee maker OK indicator as appropriate
// 7) Green light if ship intact
// 8) Set standard display if not preempted.
bool allDone;
int16_t finalSpeed, boostedSpeed, levelsDamage;
int8_t shipsHealth, myRand;
uint8_t fatalities, probFatal, probInjury;
uint8_t i;
allDone = false;
// Easiest implementation for difficulty_level is to increase touchdown speed above actual.
finalSpeed = abs ( state->speed ) + state->difficulty_level * 4;
// First Earth is a bit easier than all the others
if ( state->legend_counter == 0 ) finalSpeed -= 2;
// 1) Major crash: bug, crater, vaporized (gone).
if ( finalSpeed >= SPEED_MAJOR_CRASH ) {
allDone = true;
shipsHealth = -1;
if ( finalSpeed >= ( SPEED_MAJOR_CRASH + 2 * MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, "GOnE " );
else if ( finalSpeed >= ( SPEED_MAJOR_CRASH + MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, " CrAtr" );
else sprintf ( buf, " bU6" );
}
// 2) Rank ship's health 0 to 8
if (!allDone) {
boostedSpeed = finalSpeed + SPEED_LEVEL_INCREMENTS - 1;
levelsDamage = (int) ( ( boostedSpeed - SPEED_NO_DAMAGE ) / SPEED_LEVEL_INCREMENTS );
shipsHealth = 8 - levelsDamage;
shipsHealth = min ( shipsHealth, 8 ); // Keep between 0 and 8
shipsHealth = max ( shipsHealth, 0 );
}
state->ships_health = shipsHealth; // Remember ships health
// 3) Crew fatalities and injuries
if (!allDone) {
// Fatalies
probFatal = assignProb ( 0, 92, SPEED_FATALITY_NONE, SPEED_FATALITY_ALL, finalSpeed );
// Injuries
if ( finalSpeed <= SPEED_INJURY_FULCRUM ) {
probInjury = assignProb ( 0, INJURY_FULCRUM_PROB, SPEED_INJURY_NONE, SPEED_INJURY_FULCRUM, finalSpeed );
} else {
probInjury = assignProb ( INJURY_FULCRUM_PROB, 96, SPEED_INJURY_FULCRUM, SPEED_FATALITY_ALL, finalSpeed );
}
fatalities = 0;
state->injured = 0;
for ( i = 0; i < CREWS_COMPLIMENT; i++ ) {
myRand = gen_random_int ( 1, 100 );
if ( myRand <= probFatal ) fatalities++;
else if ( myRand <= probInjury ) state->injured++;
}
state->uninjured = CREWS_COMPLIMENT - fatalities - state->injured;
}
// 4) Special conditions: hero
if (!allDone) {
if ( (shipsHealth>=8) && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) {
state->hero_counter++;
if ( state->hero_counter==1 ) sprintf ( buf, "HErO " );
else if ( state->hero_counter == LEVEL_ACE ) sprintf ( buf, " ACE " );
else if ( state->hero_counter == LEVEL_STARBUCK ) sprintf ( buf, "STrbUC" );
else if ( state->hero_counter>99 ) sprintf ( buf, "HEr%3d", state->hero_counter );
else sprintf ( buf, "HErO%2d", state->hero_counter ); // Typical case
allDone = true;
// Two rule sets for finding Earth. Alternate between easy and hard.
int8_t my_odds, temp;
if ( state->legend_counter %2 == 0 ) my_odds = (int8_t) state->hero_counter - LEVEL_STARBUCK; // Easy
else {
temp = ( state->hero_counter - LEVEL_STARBUCK ) + HARD_EARTH_INCREMENTS - 1;
my_odds = temp / HARD_EARTH_INCREMENTS;
my_odds = min ( my_odds, MAX_HARD_EARTH_CHANCE );
}
// Display odds in weekday region if positive value
if ( my_odds > 0 ) {
char buff3 [ 5 ];
sprintf ( buff3, "%2d", my_odds );
watch_display_text( WATCH_POSITION_TOP_RIGHT, buff3 );
} else watch_display_text( WATCH_POSITION_TOP_RIGHT, " " );
if ( my_odds >= gen_random_int ( 1, 200 ) ) { // EARTH!!!! The final objective.
sprintf ( buf, "EArTH " ); // 17% within 8, 50% by 16, 79% by 24, 94% by 32 <- easy mode
state->hero_counter = 0;
state->legend_counter++;
}
// Recalculate difficulty level base on new hero_counter.
state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL;
state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit
// Write to EEPROM
write_to_lander_EEPROM(state);
}
}
// 5) Set fuel conservation indicators as appropriate
if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) watch_set_indicator ( WATCH_INDICATOR_LAP );
if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GREAT ) ) watch_set_indicator ( WATCH_INDICATOR_24H );
if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GOOD ) ) watch_set_indicator ( WATCH_INDICATOR_PM );
// 6) Set coffee maker OK indicator as appropriate
if ( shipsHealth >= 5 || ( shipsHealth >= 0 && ( gen_random_int ( 0, 3 ) != 1 ) ) ){
watch_set_indicator ( WATCH_INDICATOR_SIGNAL );
}
// 7) Green light if ship intact
if ( shipsHealth >= 8 && state->led_enabled) {
watch_set_led_green ( );
state->led_active = true;
}
// 8) Set standard display if not preempted.
if (!allDone) {
if ( ( state->injured > 0 ) || ( state->uninjured == 0 ) ) {
sprintf ( buf, "%d %2d%2d", shipsHealth, state->uninjured, state->injured );
}
else {
sprintf ( buf, "%d %2d ", shipsHealth, state->uninjured );
}
}
// Display final status.
watch_display_text(WATCH_POSITION_BOTTOM, buf );
} // End if tick_counter == 1
// Major crash - ship burning with red LED.
if ( state->ships_health < 0 && state->led_enabled) {
if ( ( gen_random_int ( 0, 1 ) != 1 ) && !last_pass ) { // Always off on last pass
// Turn on red LED.
watch_set_led_red ( );
state->led_active = true;
} else {
watch_set_led_off ( );
}
}
// Wait long enough, then allow waiting for next game.
if ( last_pass ) {
watch_set_led_off ( );
// No change to display text, allow new game to start.
state->mode = MODE_WAITING_TO_START;
// Unless it's time for monsters
uint8_t survivors = state->injured + state->uninjured;
if ( ( state->ships_health >= 0 ) && ( survivors > 0 ) &&
( gen_random_int ( -1, 3 ) >= state->ships_health ) ) {
state->mode = MODE_MONSTER;
state->tick_counter = 0;
state->monster_type = gen_random_int ( 0, MONSTER_TYPES - 1 );
}
}
} // End if MODE_DISPLAY_FINAL_STATUS
else if ( state->mode == MODE_DISPLAY_SKILL_LEVEL ) {
// Display skill level
if ( state->tick_counter == 1 ) {
sprintf ( buf, " %d", state->skill_level );
watch_display_text ( WATCH_POSITION_TOP_RIGHT, buf );
sprintf ( buf, " %d ", state->skill_level );
watch_display_text ( WATCH_POSITION_BOTTOM, buf );
}
// Wait long enough, then start game.
if ( state->tick_counter >= ( 2.0 * LANDER_TICK_FREQUENCY ) ) {
state->tick_counter = 0;
// Houston, WE ARE LAUNCHING NOW....
state->mode = MODE_PLAYING;
}
}
else if ( state->mode == MODE_FIND_EARTH_MESSAGE ) {
// Display "Find" then "Earth"
if ( state->tick_counter == 1 ) {
sprintf ( buf, " FInd " );
watch_display_text ( WATCH_POSITION_TOP_RIGHT, " " );
watch_display_text ( WATCH_POSITION_BOTTOM, buf );
}
if ( state->tick_counter == (int) ( 1.5 * LANDER_TICK_FREQUENCY + 1 ) ) {
sprintf ( buf, "EArTH " );
watch_display_text ( WATCH_POSITION_TOP_RIGHT, " " );
watch_display_text ( WATCH_POSITION_BOTTOM, buf );
}
// Wait long enough, then display skill level.
if ( state->tick_counter >= ( 3 * LANDER_TICK_FREQUENCY ) ) {
state->tick_counter = 0;
state->mode = MODE_DISPLAY_SKILL_LEVEL;
}
}
else if ( state->mode == MODE_MONSTER ) {
if ( state->tick_counter == 1 ) watch_display_text ( WATCH_POSITION_BOTTOM, lander_monster_names[state->monster_type] );
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS + 1 ) {
uint8_t my_rand;
my_rand = gen_random_int ( 0 , MONSTER_ACTIONS - 1 );
watch_display_text ( WATCH_POSITION_BOTTOM, lander_monster_actions[my_rand] );
}
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 ) { // Display 1st monster character
sprintf ( buf, "%s", lander_monster_names[state->monster_type] );
buf [1] = 0;
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 1 ) { // Display current population, close mouth
sprintf ( buf, " c%2d%2d", state->uninjured, state->injured );
watch_display_text ( WATCH_POSITION_BOTTOM, buf );
}
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 3 ) watch_display_character ( 'C', 5 ); // Open mouth
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 5 ) {
// Decision to: continue loop, end loop or eat astronaut
uint8_t survivors = state->injured + state->uninjured;
uint8_t myRand = gen_random_int ( 0, 16 );
if ( survivors == 0 ) state->mode = MODE_WAITING_TO_START;
else if ( myRand <= 1 ) { // Leave loop with survivors
sprintf ( buf, "%d %2d%2d", state->ships_health, state->uninjured, state->injured );
watch_display_text ( WATCH_POSITION_BOTTOM, buf);
state->mode = MODE_WAITING_TO_START;
} else if ( myRand <= 11 ) state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Do nothing, loop continues
else { // Eat an astronaut - welcome to the space program!
if ( state->injured > 0 && state->uninjured > 0 ) {
if ( gen_random_int ( 0,1 ) == 0 ) state->injured--;
else state->uninjured--;
}
else if ( state->injured > 0 ) state->injured--;
else state->uninjured--;
state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Re-display
}
}
else if ( state->tick_counter >= MONSTER_DISPLAY_TICKS * 4 ) state->mode = MODE_WAITING_TO_START; // Safety
} // End if MODE_MONSTER
break; // End case EVENT_TICK
case EVENT_ALARM_BUTTON_DOWN:
if ( state->mode == MODE_WAITING_TO_START ) {
// That was the go signal - start a new game!!
float numerator, denominator, timeSquared;
int16_t gravity, thrust;
float myTime, distToTop, fuel_mult;
uint8_t skill_level;
int32_t tplTop; // Top lander height for TPL calculations
movement_request_tick_frequency(LANDER_TICK_FREQUENCY);
watch_set_led_off ( ); // Safety
watch_clear_all_indicators ( );
// Randomize starting parameters
state->height = gen_random_int ( 131, 181 ) * 80;
// Per line below; see Mars Orbiter September 23, 1999
if ( gen_random_int ( 0, 8 ) == 5 ) state->height = gen_random_int ( 240, 800 ) * 80;
state->speed = gen_random_int ( -120, 35 ); // Positive is up
state->gravity = gen_random_int ( -3, -2 ) * 2; // negative downwards value
skill_level = gen_random_int ( 1, 4 ); // Precursor to fuel allocation
// Theoretical Perfect Landing (TPL) calculations start here.
myTime = (float) state->speed / (float) state->gravity; // How long to reach this speed? Don't care which way sign is.
distToTop = fabs ( 0.5 * state->gravity * myTime * myTime );
tplTop = (int) ( state->height + distToTop + 0.5 ); // Theoretical highest point based on all of speed, height and gravity.
// Time squared = ( 2 * grav * height ) / ( t*t + g*t ), where t is net acceleration with thrust on.
gravity = abs ( state->gravity );
thrust = ENGINE_THRUST + state->gravity;
numerator = 2.0 * (float) gravity * (float) tplTop;
denominator = thrust * thrust + thrust * gravity;
timeSquared = numerator / denominator;
state->fuel_tpl = (int) ( sqrt ( timeSquared ) + 0.5 ); // Fuel required for theoretical perfect landing (TPL).
if ( skill_level == 1 ) fuel_mult = 4.0; // TPL + 300%
else if ( skill_level == 2 ) fuel_mult = 2.5; // TPL + 150%
else if ( skill_level == 3 ) fuel_mult = 1.6; // TPL + 60%
else fuel_mult = 1.3; // TPL + 30%
state->fuel_start = state->fuel_tpl * fuel_mult;
state->fuel_remaining = state->fuel_start;
state->skill_level = skill_level;
state->tick_counter = 0;
if ( gen_random_int ( 1, 109 ) != 37 ) {
// Houston, approaching launch....
state->mode = MODE_DISPLAY_SKILL_LEVEL;
}
else state->mode = MODE_FIND_EARTH_MESSAGE;
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
if ( state->mode == MODE_WAITING_TO_START ) {
// Display difficulty level
watch_display_text(WATCH_POSITION_BOTTOM, lander_difficulty_names [state->difficulty_level]);
}
break;
case EVENT_LIGHT_LONG_PRESS:
if ( state->mode != MODE_WAITING_TO_START ) break;
state->led_enabled = !state->led_enabled;
if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
break;
case EVENT_LIGHT_LONG_UP:
if ( ( state->mode == MODE_WAITING_TO_START ) && ( state->legend_counter > 0 ) ) {
if ( state->legend_counter > 9 ) sprintf (buf,"EArt%2d", state->legend_counter );
else sprintf (buf,"EArth%d", state->legend_counter );
// Display legend counter
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
break;
default:
movement_default_loop_handler(event);
break;
}
if ( !state->led_active ) return true;
else return false;
}
void lander_face_resign(void *context) {
(void) context;
watch_set_led_off ( );
}
+152
View File
@@ -0,0 +1,152 @@
/*
* MIT License
*
* Copyright (c) 2024 Klingon Jane
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef LANDER_FACE_H_
#define LANDER_FACE_H_
#include "movement.h"
/*
My remake of a classic planet landing game.
Objective: Safely land the Cringeworthy.
Use your limited fuel supply to achieve a soft touch-down.
End scenarios and ship's health:
Hero They name this planet after you.
8 Life is very cozy.
7
6
5 Life is tolerable, plus some creature comforts
4
3 Marooned.
2
1
0 Ship destroyed. Life is harsh, no shelter. Giant hamsters are cute. **
Bug As in squished.
Crater They name this crater after you.
Gone As in vapourized.
Landing display format is:
Ship's health, intact crewmen, injured crewmen.
Additional data:
Crew's compliment: 13.
Low fuel warning icons: activates when 3 seconds of full thrust remains.
** Yes, hamsters are very cute. However; some eating of astronauts may occur.
Starting velocity, height and gravity are randomized each scenario.
Fuel levels randomly assigned from 1 to 4 (hardest) to match starting parameters.
A safe landing is always possible.
End of game icons:
LAP - Fantastic budgeting of fuel supply ( Required for heroic landing status. )
24H - Great budgeting of fuel supply
PM - Good budgeting of fuel supply
SIGNAL - The combination coffee and tea maker survived
Landings get progressively harder with the number of heroic landings made.
Number of heroic landings are remembered.
Heroic
Landings Status
0 Normal
3 Hard ( first difficulty increase )
6 Harder ( final difficulty increase )
8 Ace
11 ??????
Save yourself. Save the coffee maker.
END of standard training manual
*/
/*
What is really going on here?
The fleet is lost. You are a newbie pilot making a name for yourself.
Objective: Find Earth.
After reaching ?????? status, future heroic sorties will have 'some' chance in 200
of finding Earth.
Your chances improve by 1 chance in 200 for each subsequent Heroic Landing (HL).
Completing HL 12 will give you 1 chance in 200, for that landing.
HL 13 will give you 2 chances in 200, for that landing.
HL 14 will give you 3 chances in 200, for that landing.
HL 20 will give you 9 chances in 200, for that landing, and so on.
At these higher levels, your chances in 200 are displayed in the upper right corner on a heroic landing.
For wannabe pilots only: The HL counter can be reset by crashing three consecutive
missions without touching the thrust button. ( 6 to reset Earth-found counter )
Find Earth. Save Humanity.
*/
typedef struct {
int32_t height;
int16_t speed; // Positive is up
uint16_t tick_counter; // For minimum delays
uint16_t fuel_start;
uint16_t fuel_remaining;
uint16_t fuel_tpl; // Fuel required for theoretical perfect landing
uint16_t fuel_score; // 100 is perfect; higher is less perfect
int8_t gravity; // negative downwards value
bool led_enabled; // Can the led be turned on?
bool led_active; // Did we use it this scenario?
uint8_t mode; // 0 Pre-launch waiting, 1 show level, 2 playing, 3 touchdown blank, 4 final display, 5 monster
uint8_t skill_level; // 1 thru 4. Dictates fuel alloted
int8_t ships_health; // 0 thru 8. -1 = major crash
uint8_t hero_counter; // Total heroic landings ever
uint8_t legend_counter; // Historic events counter ( Earth )
uint8_t difficulty_level; // Based on hero_counter
uint8_t reset_counter; // Can reset hero_counter by crashing using zero fuel several consecutive scenarios
uint8_t monster_type; // Which monster is hungry?
uint8_t uninjured; // OK survivors
uint8_t injured; // Hurt survivors
} lander_state_t;
void lander_face_setup(uint8_t watch_face_index, void ** context_ptr);
void lander_face_activate(void *context);
bool lander_face_loop(movement_event_t event, void *context);
void lander_face_resign(void *context);
#define lander_face ((const watch_face_t){ \
lander_face_setup, \
lander_face_activate, \
lander_face_loop, \
lander_face_resign, \
NULL, \
})
#endif // LANDER_FACE_H_
+12 -3
View File
@@ -184,10 +184,19 @@ bool moon_phase_face_loop(movement_event_t event, void *context) {
state->offset += 86400; state->offset += 86400;
_update(state, state->offset); _update(state, state->offset);
break; break;
case EVENT_ALARM_LONG_PRESS: case EVENT_ALARM_LONG_PRESS:
state->offset = 0; state->offset = 0;
_update(state, state->offset); _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: case EVENT_TIMEOUT:
// QUESTION: Should timeout reset offset to 0? // QUESTION: Should timeout reset offset to 0?
break; break;
@@ -47,6 +47,9 @@
* each button press, and both the text and the graphical representation will * 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 * 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. * 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" #include "movement.h"
+583
View File
@@ -0,0 +1,583 @@
/*
* MIT License
*
* Copyright (c) 2024 <David Volovskiy>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include <string.h>
#include "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);
}
+71
View File
@@ -0,0 +1,71 @@
/*
* 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.
*/
#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_
@@ -80,6 +80,7 @@ static void pulsometer_display_measurement(pulsometer_state_t *pulsometer) {
char buf[5]; char buf[5];
int16_t value = pulsometer->pulses; int16_t value = pulsometer->pulses;
if (value < 0) value = 0;
if (value > 9999) value = 9999; if (value > 9999) value = 9999;
snprintf(buf, sizeof(buf), "%-4hd", value); snprintf(buf, sizeof(buf), "%-4hd", value);
@@ -23,6 +23,7 @@
*/ */
#include "simon_face.h" #include "simon_face.h"
#include "delay.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -47,18 +48,19 @@ static inline uint8_t _simon_get_rand_num(uint8_t num_values) {
} }
static void _simon_clear_display(simon_state_t *state) { static void _simon_clear_display(simon_state_t *state) {
if (state->playing_state == SIMON_NOT_PLAYING) { watch_clear_display();
watch_display_string(" ", 0); if (state->playing_state != SIMON_NOT_PLAYING) {
} else { sprintf(_simon_display_buf, "%2d", state->sequence_length);
sprintf(_simon_display_buf, " %2d ", state->sequence_length); watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf);
watch_display_string(_simon_display_buf, 0);
} }
} }
static void _simon_not_playing_display(simon_state_t *state) { static void _simon_not_playing_display(simon_state_t *state) {
_simon_clear_display(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) if (!state->soundOff)
watch_set_indicator(WATCH_INDICATOR_BELL); watch_set_indicator(WATCH_INDICATOR_BELL);
else else
@@ -67,14 +69,13 @@ static void _simon_not_playing_display(simon_state_t *state) {
watch_set_indicator(WATCH_INDICATOR_SIGNAL); watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else else
watch_clear_indicator(WATCH_INDICATOR_SIGNAL); watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
watch_display_string(_simon_display_buf, 0);
switch (state->mode) switch (state->mode)
{ {
case SIMON_MODE_EASY: case SIMON_MODE_EASY:
watch_display_string("E", 9); watch_display_text(WATCH_POSITION_SECONDS, " E");
break; break;
case SIMON_MODE_HARD: case SIMON_MODE_HARD:
watch_display_string("H", 9); watch_display_text(WATCH_POSITION_SECONDS, " H");
break; break;
default: default:
break; break;
@@ -90,24 +91,27 @@ static void _simon_reset(simon_state_t *state) {
static void _simon_display_note(SimonNote note, 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) { switch (note) {
case SIMON_LED_NOTE: case SIMON_LED_NOTE:
ndtemplate = "LI%2d "; watch_display_text(WATCH_POSITION_TOP_LEFT, "LI");
break; break;
case SIMON_ALARM_NOTE: case SIMON_ALARM_NOTE:
ndtemplate = " %2d AL"; watch_display_text(WATCH_POSITION_SECONDS, "AL");
break; break;
case SIMON_MODE_NOTE: case SIMON_MODE_NOTE:
ndtemplate = " %2dDE "; watch_display_text_with_fallback(WATCH_POSITION_HOURS, "Md", "DE");
break;
default:
break; 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) { 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) { switch (note) {
case SIMON_LED_NOTE: case SIMON_LED_NOTE:
if (!state->lightOff) watch_set_led_yellow(); if (!state->lightOff) watch_set_led_yellow();
if (state->soundOff) if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
delay_ms(_delay_beep); delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
break; break;
case SIMON_MODE_NOTE: case SIMON_MODE_NOTE:
if (!state->lightOff) watch_set_led_red(); if (!state->lightOff) watch_set_led_red();
if (state->soundOff) if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep);
delay_ms(_delay_beep); delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep);
break; break;
case SIMON_ALARM_NOTE: case SIMON_ALARM_NOTE:
if (!state->lightOff) watch_set_led_green(); if (!state->lightOff) watch_set_led_green();
if (state->soundOff) if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep);
delay_ms(_delay_beep); delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep);
break; break;
case SIMON_WRONG_NOTE: case SIMON_WRONG_NOTE:
if (state->soundOff) if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_A1, 800);
delay_ms(800); delay_ms(800);
else
watch_buzzer_play_note(BUZZER_NOTE_A1, 800);
break; break;
} }
watch_set_led_off(); 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) { if (note != SIMON_WRONG_NOTE) {
_simon_clear_display(state); _simon_clear_display(state);
if (!skip_rest) { 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 simon_face_activate(void *context) {
(void) settings;
(void) context; (void) context;
simon_state_t *state = (simon_state_t *)context; simon_state_t *state = (simon_state_t *)context;
_simon_change_speed(state); _simon_change_speed(state);
@@ -95,8 +95,8 @@ void simon_face_activate(void *context);
bool simon_face_loop(movement_event_t event, void *context); bool simon_face_loop(movement_event_t event, void *context);
void simon_face_resign(void *context); void simon_face_resign(void *context);
#define simon_face \ #define simon_face \
((const watch_face_t){ \ ((const watch_face_t){ \
simon_face_setup, \ simon_face_setup, \
simon_face_activate, \ simon_face_activate, \
simon_face_loop, \ simon_face_loop, \
@@ -26,6 +26,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "simple_coin_flip_face.h" #include "simple_coin_flip_face.h"
#include "delay.h"
void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) { void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index; (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) { 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) { 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(); watch_clear_display();
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_display_text(WATCH_POSITION_BOTTOM, " Flip"); 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 loopruns = 0; loopruns<2; loopruns++) {
for(int i = 0; i<3; i++) { for(int i = 0; i<3; i++) {
watch_clear_display(); 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) { if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
int j_len = 2; int j_len = 2;
int pixels[3][4][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) { static void _blink_face_update_lcd(simple_coin_flip_face_state_t *state) {
(void) state;
watch_clear_display(); watch_clear_display();
load_animation(); load_animation();
watch_clear_display(); watch_clear_display();
@@ -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 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 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. // 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. // 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. // 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; timestamp += 86400;
scratch_time = watch_utility_date_time_from_unix_time(timestamp, 0); scratch_time = watch_utility_date_time_from_unix_time(timestamp, 0);
} }
+16 -6
View File
@@ -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) { bool tally_face_loop(movement_event_t event, void *context) {
tally_state_t *state = (tally_state_t *)context; tally_state_t *state = (tally_state_t *)context;
static bool using_led = false; static bool using_led = false;
static int8_t beep_sequence[] = {
0, 2,
BUZZER_NOTE_REST, 3,
0, 2,
0
};
if (using_led) { if (using_led) {
if(!HAL_GPIO_BTN_MODE_read() && !HAL_GPIO_BTN_LIGHT_read() && !HAL_GPIO_BTN_ALARM_read()) 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 state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index
_init_val = true; _init_val = true;
//play a reset tune //play a reset tune
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30); if (movement_button_should_sound()) {
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30); beep_sequence[0] = BUZZER_NOTE_G6;
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30); beep_sequence[4] = BUZZER_NOTE_E6;
movement_play_sequence(beep_sequence, 0);
}
print_tally(state, movement_button_should_sound()); print_tally(state, movement_button_should_sound());
} }
break; break;
@@ -168,9 +176,11 @@ bool tally_face_loop(movement_event_t event, void *context) {
if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){ if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){
state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE(); state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE();
state->tally_idx = _tally_default[state->tally_default_idx]; 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()) {
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30); beep_sequence[0] = BUZZER_NOTE_E6;
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30); beep_sequence[4] = BUZZER_NOTE_G6;
movement_play_sequence(beep_sequence, 0);
}
print_tally(state, movement_button_should_sound()); print_tally(state, movement_button_should_sound());
} }
else{ else{
+3 -2
View File
@@ -280,6 +280,9 @@ void tarot_face_setup(uint8_t watch_face_index, void ** context_ptr) {
if (*context_ptr == NULL) { if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(tarot_state_t)); *context_ptr = malloc(sizeof(tarot_state_t));
memset(*context_ptr, 0, 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 // Emulator only: Seed random number generator
#if __EMSCRIPTEN__ #if __EMSCRIPTEN__
@@ -292,8 +295,6 @@ void tarot_face_activate(void *context) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "Tarot", "TA"); watch_display_text_with_fallback(WATCH_POSITION_TOP, "Tarot", "TA");
init_deck(state); init_deck(state);
state->num_cards_to_draw = 3;
state->major_arcana_only = true;
} }
bool tarot_face_loop(movement_event_t event, void *context) { bool tarot_face_loop(movement_event_t event, void *context) {
+186
View File
@@ -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 <stdlib.h>
#include <string.h>
#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;
}
+82
View File
@@ -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_
+1 -2
View File
@@ -36,7 +36,6 @@
#include <string.h> #include <string.h>
#include "totp_face.h" #include "totp_face.h"
#include "watch.h" #include "watch.h"
#include "watch_utility.h"
#include "TOTP.h" #include "TOTP.h"
#include "base32.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() { 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) { void totp_face_setup(uint8_t watch_face_index, void ** context_ptr) {
+1 -2
View File
@@ -30,7 +30,6 @@
#include "base32.h" #include "base32.h"
#include "watch.h" #include "watch.h"
#include "watch_utility.h"
#include "filesystem.h" #include "filesystem.h"
#include "totp_lfs_face.h" #include "totp_lfs_face.h"
@@ -253,7 +252,7 @@ void totp_lfs_face_activate(void *context) {
} }
#endif #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); totp_face_set_record(totp_state, 0);
} }
+23 -13
View File
@@ -1,17 +1,27 @@
/* /*
* MIT License
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. *
* Copyright (c) 2025 kbc-yam
Light button: Subtract one year from the Japanese Era. *
Start/Stop button: Add one year to the Japanese Era. * Permission is hereby granted, free of charge, to any person obtaining a
Button operations support long-press functionality. * copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
Japanese Era Notations: * without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit
r : REIWA (令和) * persons to whom the Software is furnished to do so, subject to the
h : HEISEI (平成) * following conditions:
s : SHOWA(昭和) *
*/ * The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
+41
View File
@@ -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_ #ifndef WAREKI_FACE_H_
#define 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" #include "movement.h"
#define REIWA_LIMIT 2018 + 99 #define REIWA_LIMIT 2018 + 99
+8 -7
View File
@@ -256,7 +256,7 @@ static void reset_board(wordle_state_t *state) {
static void display_title(wordle_state_t *state) { static void display_title(wordle_state_t *state) {
state->curr_screen = WORDLE_SCREEN_TITLE; 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_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, "WordLE"); watch_display_text(WATCH_POSITION_BOTTOM, "WordLE");
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); 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 #else
sprintf(buf, "St%4d", state->streak); sprintf(buf, "St%4d", state->streak);
#endif #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_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, buf); watch_display_text(WATCH_POSITION_BOTTOM, buf);
watch_set_colon(); watch_set_colon();
@@ -304,7 +304,7 @@ static void display_wait(wordle_state_t *state) {
else { // Streak too long to display in top-right else { // Streak too long to display in top-right
watch_display_text(WATCH_POSITION_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 "); watch_display_text(WATCH_POSITION_BOTTOM, " WaIt ");
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); 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) { static void display_lose(wordle_state_t *state, uint8_t subsecond) {
char buf[10]; char buf[10];
sprintf(buf," %s", subsecond % 2 ? _valid_words[state->curr_answer] : " "); 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); watch_display_text(WATCH_POSITION_BOTTOM, buf);
} }
static void display_win(wordle_state_t *state, uint8_t subsecond) { static void display_win(wordle_state_t *state, uint8_t subsecond) {
(void) state; (void) state;
char buf[10]; char buf[10];
sprintf(buf," %s ", subsecond % 2 ? "NICE" : "JOb "); sprintf(buf," %s ", subsecond % 2 ? "NICE" : "JOb ");
watch_display_text(WATCH_POSITION_TOP, "W "); 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); 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 wordle_face_resign(void *context) {
(void) context; (void) context;
} }
+204
View File
@@ -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 <stdlib.h>
#include <string.h>
#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;
}
+47
View File
@@ -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, \
})
+55 -113
View File
@@ -47,9 +47,6 @@ typedef struct {
// Selected program // Selected program
chirpy_demo_program_t 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 // Used by chirpy encoder during transmission
chirpy_encoder_state_t encoder_state; 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 uint8_t *curr_data_ptr;
static uint16_t curr_data_ix; static uint16_t curr_data_ix;
static uint16_t curr_data_len; static uint16_t curr_data_len;
static chirpy_demo_state_t *curr_state;
static uint8_t _cdf_get_next_byte(uint8_t *next_byte) { static uint8_t _cdf_get_next_byte(uint8_t *next_byte) {
if (curr_data_ix == curr_data_len) if (curr_data_ix == curr_data_len)
@@ -199,59 +160,60 @@ static uint8_t _cdf_get_next_byte(uint8_t *next_byte) {
return 1; return 1;
} }
static void _cdf_countdown_tick(void *context) { static void _cdf_on_chirping_done(void) {
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context; if (curr_state) {
chirpy_tick_state_t *tick_state = &state->tick_state; curr_state->mode = CDM_CHOOSE;
// 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;
} }
// Sound or turn off buzzer watch_clear_indicator(WATCH_INDICATOR_BELL);
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;
} }
static void _cdm_setup_chirp(chirpy_demo_state_t *state) { static bool _cdm_raw_source_fn(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration) {
// We want frequent callbacks from now on // Beep countdown
movement_request_tick_frequency(64); 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); watch_set_indicator(WATCH_INDICATOR_BELL);
state->mode = CDM_CHIRPING; state->mode = CDM_CHIRPING;
// Set up tick state; start with countdown
state->tick_state.tick_count = -1; // Set up the data
state->tick_state.tick_compare = 8; curr_state = state;
state->tick_state.seq_pos = 0; curr_data_ix = 0;
state->tick_state.tick_fun = _cdf_countdown_tick; 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) { 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: case EVENT_ACTIVATE:
_cdf_update_lcd(state); _cdf_update_lcd(state);
break; break;
case EVENT_MODE_BUTTON_UP: case EVENT_LIGHT_BUTTON_DOWN:
// Do not exit face while we're chirping
if (state->mode != CDM_CHIRPING) {
movement_move_to_next_face();
}
break;
case EVENT_LIGHT_BUTTON_UP: case EVENT_LIGHT_BUTTON_UP:
// We don't do light. // We don't do light.
break; break;
@@ -286,10 +243,6 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
state->program = CDP_CLEAR; state->program = CDP_CLEAR;
_cdf_update_lcd(state); _cdf_update_lcd(state);
} }
// If chirping: stoppit
else if (state->mode == CDM_CHIRPING) {
_cdf_quit_chirping(state);
}
break; break;
case EVENT_ALARM_LONG_PRESS: case EVENT_ALARM_LONG_PRESS:
// If in choose mode: start chirping // 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_force_led_off();
movement_move_to_next_face(); movement_move_to_next_face();
} else { } else {
_cdm_setup_chirp(state); _cdm_start_transmission(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);
} }
} }
break; break;
@@ -317,15 +261,13 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
if (state->mode != CDM_CHIRPING) { if (state->mode != CDM_CHIRPING) {
movement_move_to_face(0); movement_move_to_face(0);
} }
// fall through
default: default:
movement_default_loop_handler(event);
break; break;
} }
// Return true if the watch can enter standby mode. False needed when chirping. return true;
if (state->mode == CDM_CHIRPING)
return false;
else
return true;
} }
void chirpy_demo_face_resign(void *context) { void chirpy_demo_face_resign(void *context) {
@@ -86,6 +86,13 @@ void activity_logging_face_activate(void *context) {
bool activity_logging_face_loop(movement_event_t event, void *context) { bool activity_logging_face_loop(movement_event_t event, void *context) {
activity_logging_state_t *state = (activity_logging_state_t *)context; activity_logging_state_t *state = (activity_logging_state_t *)context;
switch (event.event_type) { 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: case EVENT_ALARM_BUTTON_DOWN:
state->display_index = (state->display_index + 1) % ACTIVITY_LOGGING_NUM_DAYS; state->display_index = (state->display_index + 1) % ACTIVITY_LOGGING_NUM_DAYS;
// fall through // fall through
@@ -40,6 +40,8 @@
* *
* A short press of the Alarm button moves backwards in the data log, showing yesterday's active minutes, * 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. * 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.
* *
*/ */
+1 -1
View File
@@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state)
lis2dw_fifo_t fifo; lis2dw_fifo_t fifo;
float x = 0, y = 0, z = 0; float x = 0, y = 0, z = 0;
lis2dw_read_fifo(&fifo); lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT / DISPLAY_FREQUENCY);
if (fifo.count == 0) { if (fifo.count == 0) {
return; return;
} }
+7 -16
View File
@@ -27,7 +27,6 @@
#include <math.h> #include <math.h>
#include "finetune_face.h" #include "finetune_face.h"
#include "nanosec_face.h" #include "nanosec_face.h"
#include "watch_utility.h"
#include "delay.h" #include "delay.h"
extern nanosec_state_t nanosec_state; extern nanosec_state_t nanosec_state;
@@ -51,7 +50,7 @@ void finetune_face_activate(void *context) {
} }
static float finetune_get_hours_passed(void) { 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; return (current_time - nanosec_state.last_correction_time) / 3600.0f;
} }
@@ -64,7 +63,7 @@ static void finetune_update_display(void) {
if (finetune_page == 0) { if (finetune_page == 0) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "FTU", "FT"); 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); sprintf(buf, "%04d%02d", abs(total_adjustment), date_time.unit.second);
watch_display_text(WATCH_POSITION_BOTTOM, buf); watch_display_text(WATCH_POSITION_BOTTOM, buf);
@@ -106,17 +105,9 @@ static void finetune_adjust_subseconds(int delta) {
watch_rtc_enable(false); watch_rtc_enable(false);
delay_ms(delta); delay_ms(delta);
if (delta > 500) { if (delta > 500) {
watch_date_time_t date_time = watch_rtc_get_date_time(); uint32_t timestamp = movement_get_utc_timestamp();
date_time.unit.second = (date_time.unit.second + 1) % 60; timestamp += 1;
if (date_time.unit.second == 0) { // Overflow movement_set_utc_timestamp(timestamp);
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);
} }
watch_rtc_enable(true); watch_rtc_enable(true);
} }
@@ -126,7 +117,7 @@ static void finetune_update_correction_time(void) {
nanosec_state.freq_correction += roundf(nanosec_get_aging() * 100); nanosec_state.freq_correction += roundf(nanosec_get_aging() * 100);
// Remember when we last corrected time // 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(); nanosec_save();
movement_move_to_face(0); // Go to main face after saving settings 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 // We flash green LED once per minute to measure clock error, when we are not on first screen
if (finetune_page!=0) { if (finetune_page!=0) {
watch_date_time_t date_time; 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) { if (date_time.unit.second == 0) {
watch_set_led_green(); watch_set_led_green();
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__
-5
View File
@@ -44,11 +44,6 @@
* worry about aging only on second/third years of watch calibration (if you * 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). * 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: * For full usage instructions, please refer to the wiki:
* https://www.sensorwatch.net/docs/watchfaces/nanosec/ * https://www.sensorwatch.net/docs/watchfaces/nanosec/
*/ */
+4 -6
View File
@@ -27,7 +27,6 @@
#include <math.h> #include <math.h>
#include "nanosec_face.h" #include "nanosec_face.h"
#include "filesystem.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_residual = 0; // Dithering 0.1ppm correction, does not need to be configured.
int16_t freq_correction_previous = -30000; 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) { static void nanosec_init_profile(void) {
nanosec_changed = true; nanosec_changed = true;
nanosec_state.correction_cadence = 10; nanosec_state.correction_cadence = 10;
watch_date_time_t date_time = watch_rtc_get_date_time(); nanosec_state.last_correction_time = movement_get_utc_timestamp();
nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(date_time, 0);
// init data after changing profile - do that once per profile selection // init data after changing profile - do that once per profile selection
switch (nanosec_state.correction_profile) { 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 float nanosec_get_aging() // Returns aging correction in ppm
{ {
watch_date_time_t date_time = watch_rtc_get_date_time(); uint32_t timestamp = movement_get_utc_timestamp();
float years = (watch_utility_date_time_to_unix_time(date_time, 0) - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune float years = (timestamp - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune
return years*nanosec_state.aging_ppm_pa/100.0f; 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. // No need for background correction if we are on profile 0 - static hardware correction.
if (nanosec_state.correction_profile != 0) { 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; retval.wants_background_task = date_time.unit.minute % nanosec_state.correction_cadence == 0;
} }
+4 -6
View File
@@ -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()); current_offset = movement_get_current_timezone_offset_for_zone(movement_get_timezone_index());
return; return;
case 0: // year case 0: // year
date_time.unit.year = ((date_time.unit.year % 60) + 1); date_time.unit.year = (date_time.unit.year + 1) % 60;
break; break;
case 1: // month case 1: // month
date_time.unit.month = (date_time.unit.month % 12) + 1; 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(); watch_date_time_t date_time = movement_get_local_date_time();
switch (event.event_type) { switch (event.event_type) {
case EVENT_ACTIVATE:
break;
case EVENT_TICK: case EVENT_TICK:
if (_quick_ticks_running) { if (_quick_ticks_running) {
if (HAL_GPIO_BTN_ALARM_read()) _handle_alarm_button(date_time, current_page); 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: case EVENT_ALARM_LONG_UP:
_abort_quick_ticks(); _abort_quick_ticks();
break; break;
case EVENT_MODE_BUTTON_UP:
_abort_quick_ticks();
movement_move_to_next_face();
return false;
case EVENT_LIGHT_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_DOWN:
current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS; current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS;
*((uint8_t *)context) = current_page; *((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 set_time_face_resign(void *context) {
(void) context; (void) context;
watch_set_led_off();
movement_store_settings(); movement_store_settings();
movement_request_tick_frequency(1);
} }
+67 -3
View File
@@ -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) { static void timeout_setting_display(uint8_t subsecond) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "TMOUt", "TO"); watch_display_text_with_fallback(WATCH_POSITION_TOP, "TMOUt", "TO");
if (subsecond % 2) { 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; settings_state_t *state = (settings_state_t *)*context_ptr;
int8_t current_setting = 0; 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 #ifdef BUILD_GIT_HASH
state->num_settings++; state->num_settings++;
#endif #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].display = beep_setting_display;
state->settings_screens[current_setting].advance = beep_setting_advance; state->settings_screens[current_setting].advance = beep_setting_advance;
current_setting++; 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].display = timeout_setting_display;
state->settings_screens[current_setting].advance = timeout_setting_advance; state->settings_screens[current_setting].advance = timeout_setting_advance;
current_setting++; current_setting++;
@@ -322,7 +386,7 @@ bool settings_face_loop(movement_event_t event, void *context) {
case EVENT_MODE_BUTTON_UP: case EVENT_MODE_BUTTON_UP:
movement_force_led_off(); movement_force_led_off();
movement_move_to_next_face(); movement_move_to_next_face();
return false; return true;
case EVENT_ALARM_BUTTON_UP: case EVENT_ALARM_BUTTON_UP:
state->settings_screens[state->current_page].advance(); state->settings_screens[state->current_page].advance();
break; break;
@@ -339,7 +403,7 @@ bool settings_face_loop(movement_event_t event, void *context) {
movement_force_led_on(color.red | color.red << 4, movement_force_led_on(color.red | color.red << 4,
color.green | color.green << 4, color.green | color.green << 4,
color.blue | color.blue << 4); color.blue | color.blue << 4);
return false; return true;
} else { } else {
movement_force_led_off(); movement_force_led_off();
return true; return true;
+6
View File
@@ -44,6 +44,12 @@
* This setting allows you to choose whether the Mode button should emit * 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 * a beep when pressed, and if so, how loud it should be. Options are
* "Y" for yes and "N" for no. * "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. * TO / Tmout - Timeout.
* Sets the time until screens that time out (like Settings and Time Set) * Sets the time until screens that time out (like Settings and Time Set)
+136
View File
@@ -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 <stddef.h>
#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);
}
+13 -12
View File
@@ -41,7 +41,7 @@ void sleep(const uint8_t mode) {
} }
void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool level) { 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()) { if (pin == HAL_GPIO_BTN_ALARM_pin()) {
HAL_GPIO_BTN_ALARM_in(); 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 // disable the RTC
RTC->MODE2.CTRLA.bit.ENABLE = 0; RTC->MODE0.CTRLA.bit.ENABLE = 0;
while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled
// update the configuration // update the configuration
RTC->MODE2.TAMPCTRL.reg = config; RTC->MODE0.TAMPCTRL.reg = config;
// re-enable the RTC // re-enable the RTC
RTC->MODE2.CTRLA.bit.ENABLE = 1; RTC->MODE0.CTRLA.bit.ENABLE = 1;
NVIC_ClearPendingIRQ(RTC_IRQn); NVIC_ClearPendingIRQ(RTC_IRQn);
NVIC_EnableIRQ(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) { 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()) { if (pin == HAL_GPIO_BTN_ALARM_pin()) {
btn_alarm_callback = NULL; btn_alarm_callback = NULL;
@@ -101,14 +101,14 @@ void watch_disable_extwake_interrupt(uint8_t pin) {
} }
// disable the RTC // disable the RTC
RTC->MODE2.CTRLA.bit.ENABLE = 0; RTC->MODE0.CTRLA.bit.ENABLE = 0;
while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled
// update the configuration // update the configuration
RTC->MODE2.TAMPCTRL.reg = config; RTC->MODE0.TAMPCTRL.reg = config;
// re-enable the RTC // 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) { 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) { static void _watch_disable_all_peripherals_except_slcd(void) {
_watch_disable_tcc(); watch_disable_leds();
watch_disable_buzzer();
watch_disable_adc(); watch_disable_adc();
watch_disable_external_interrupts(); watch_disable_external_interrupts();
+30 -17
View File
@@ -38,27 +38,31 @@ void watch_disable_i2c(void) {
i2c_disable(); i2c_disable();
} }
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) {
i2c_write(addr, buf, length); return (int8_t)i2c_write(addr, buf, length);
} }
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) {
i2c_read(addr, buf, 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]; uint8_t buf[2];
buf[0] = reg; buf[0] = reg;
buf[1] = data; 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 watch_i2c_read8(int16_t addr, uint8_t reg) {
uint8_t data; uint8_t data;
watch_i2c_send(addr, (uint8_t *)&reg, 1); if (watch_i2c_send(addr, (uint8_t *)&reg, 1) != 0) {
watch_i2c_receive(addr, (uint8_t *)&data, 1); return 0;
}
if (watch_i2c_receive(addr, (uint8_t *)&data, 1) != 0) {
return 0;
}
return data; 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 watch_i2c_read16(int16_t addr, uint8_t reg) {
uint16_t data; uint16_t data;
watch_i2c_send(addr, (uint8_t *)&reg, 1); if (watch_i2c_send(addr, (uint8_t *)&reg, 1) != 0) {
watch_i2c_receive(addr, (uint8_t *)&data, 2); return 0;
}
if (watch_i2c_receive(addr, (uint8_t *)&data, 2) != 0) {
return 0;
}
return data; return data;
} }
@@ -76,18 +83,24 @@ uint32_t watch_i2c_read24(int16_t addr, uint8_t reg) {
uint32_t data; uint32_t data;
data = 0; data = 0;
watch_i2c_send(addr, (uint8_t *)&reg, 1); if (watch_i2c_send(addr, (uint8_t *)&reg, 1) != 0) {
watch_i2c_receive(addr, (uint8_t *)&data, 3); return 0;
}
if (watch_i2c_receive(addr, (uint8_t *)&data, 3) != 0) {
return 0;
}
return data << 8; return data << 8;
} }
uint32_t watch_i2c_read32(int16_t addr, uint8_t reg) { uint32_t watch_i2c_read32(int16_t addr, uint8_t reg) {
uint32_t data; uint32_t data;
watch_i2c_send(addr, (uint8_t *)&reg, 1); if (watch_i2c_send(addr, (uint8_t *)&reg, 1) != 0) {
watch_i2c_receive(addr, (uint8_t *)&data, 4); return 0;
}
if (watch_i2c_receive(addr, (uint8_t *)&data, 4) != 0) {
return 0;
}
return data; return data;
} }
+204 -39
View File
@@ -2,6 +2,7 @@
* MIT License * MIT License
* *
* Copyright (c) 2020 Joey Castillo * Copyright (c) 2020 Joey Castillo
* Copyright (c) 2025 Alessandro Genova
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -23,11 +24,36 @@
*/ */
#include <stddef.h> #include <stddef.h>
#include <limits.h>
#include "watch_rtc.h" #include "watch_rtc.h"
#include "watch_private.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]; watch_cb_t tick_callbacks[8];
comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB];
watch_cb_t alarm_callback; watch_cb_t alarm_callback;
watch_cb_t btn_alarm_callback; watch_cb_t btn_alarm_callback;
watch_cb_t a2_callback; watch_cb_t a2_callback;
@@ -46,14 +72,77 @@ void _watch_rtc_init(void) {
#endif #endif
rtc_enable(); rtc_enable();
rtc_configure_callback(watch_rtc_callback); 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) { 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) { 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) { 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. // this also maps nicely to an index for our list of tick callbacks.
tick_callbacks[per_n] = callback; tick_callbacks[per_n] = callback;
NVIC_ClearPendingIRQ(RTC_IRQn); // NVIC_ClearPendingIRQ(RTC_IRQn);
NVIC_EnableIRQ(RTC_IRQn); // NVIC_EnableIRQ(RTC_IRQn);
RTC->MODE2.INTENSET.reg = 1 << per_n; RTC->MODE0.INTENSET.reg = 1 << per_n;
} }
void watch_rtc_disable_periodic_callback(uint8_t frequency) { void watch_rtc_disable_periodic_callback(uint8_t frequency) {
if (__builtin_popcount(frequency) != 1) return; if (__builtin_popcount(frequency) != 1) return;
uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24); 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) { 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) { void watch_rtc_disable_all_periodic_callbacks(void) {
watch_rtc_disable_matching_periodic_callbacks(0xFF); 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) { void watch_rtc_schedule_next_comp(void) {
RTC->MODE2.Mode2Alarm[0].ALARM.reg = alarm_time.reg; rtc_counter_t curr_counter = watch_rtc_get_counter();
RTC->MODE2.Mode2Alarm[0].MASK.reg = mask;
RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0; // We want to ensure we never miss any registered callbacks,
alarm_callback = callback; // so if a callback counter has just passed but didn't fire, give it a chance to fire.
NVIC_ClearPendingIRQ(RTC_IRQn); rtc_counter_t lax_curr_counter = curr_counter - RTC_COMP_GRACE_PERIOD;
NVIC_EnableIRQ(RTC_IRQn);
RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0; 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) { void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) {
RTC->MODE2.INTENCLR.reg = RTC_MODE2_INTENCLR_ALARM0; 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) { void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) {
uint16_t interrupt_enabled = RTC->MODE2.INTENSET.reg; 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. // handle the tick callback first, it's what we do the most.
// start from PER7, the 1 Hz tick. // start from PER7, the 1 Hz tick.
for(int8_t i = 7; i >= 0; i--) { 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) { if (tick_callbacks[i] != NULL) {
tick_callbacks[i](); 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. // 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 (reason & RTC_TAMPID_TAMPID2) {
if (btn_alarm_callback != NULL) btn_alarm_callback(); if (btn_alarm_callback != NULL) btn_alarm_callback();
} else if (reason & RTC_TAMPID_TAMPID1) { } else if (reason & RTC_TAMPID_TAMPID1) {
@@ -161,25 +316,36 @@ void watch_rtc_callback(uint16_t interrupt_status) {
} else if (reason & RTC_TAMPID_TAMPID0) { } else if (reason & RTC_TAMPID_TAMPID0) {
if (a4_callback != NULL) a4_callback(); if (a4_callback != NULL) a4_callback();
} }
RTC->MODE2.TAMPID.reg = reason; RTC->MODE0.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 ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) {
if (alarm_callback != NULL) { for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
alarm_callback(); 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) { void watch_rtc_enable(bool en) {
// Writing it twice - as it's quite dangerous operation. // Writing it twice - as it's quite dangerous operation.
// If write fails - we might hang with RTC off, which means no recovery possible // If write fails - we might hang with RTC off, which means no recovery possible
while (RTC->MODE2.SYNCBUSY.reg); while (RTC->MODE0.SYNCBUSY.reg);
RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0; RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0;
while (RTC->MODE2.SYNCBUSY.reg); while (RTC->MODE0.SYNCBUSY.reg);
RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0; RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0;
while (RTC->MODE2.SYNCBUSY.reg); while (RTC->MODE0.SYNCBUSY.reg);
} }
void watch_rtc_freqcorr_write(int16_t value, int16_t sign) { 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.VALUE = value;
data.bit.SIGN = sign; 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 // We do not sycnronize. We are not in a hurry
} }
+5 -1
View File
@@ -252,7 +252,7 @@ void watch_enable_display(void) {
slcd_clear(); slcd_clear();
if (_installed_display == WATCH_LCD_TYPE_CUSTOM) { if (_installed_display == WATCH_LCD_TYPE_CUSTOM) {
slcd_set_contrast(4); slcd_set_contrast(0);
} else { } else {
slcd_set_contrast(9); slcd_set_contrast(9);
} }
@@ -260,6 +260,10 @@ void watch_enable_display(void) {
slcd_enable(); slcd_enable();
} }
void watch_disable_display(void) {
slcd_disable();
}
inline void watch_set_pixel(uint8_t com, uint8_t seg) { inline void watch_set_pixel(uint8_t com, uint8_t seg) {
slcd_set_segment(com, seg); slcd_set_segment(com, seg);
} }
+225 -60
View File
@@ -27,14 +27,30 @@
#include "tcc.h" #include "tcc.h"
#include "tc.h" #include "tc.h"
void _watch_enable_tcc(void); static void _watch_enable_tcc(void);
void cb_watch_buzzer_seq(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 uint16_t _seq_position;
static int8_t _tone_ticks, _repeat_counter; static int8_t _tone_ticks, _repeat_counter;
static bool _callback_running = false;
static int8_t *_sequence; 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 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) { static void _tcc_write_RUNSTDBY(bool value) {
// enables or disables RUNSTDBY of the tcc // enables or disables RUNSTDBY of the tcc
@@ -46,13 +62,11 @@ static void _tcc_write_RUNSTDBY(bool value) {
static inline void _tc0_start() { static inline void _tc0_start() {
// start the TC0 timer // start the TC0 timer
tc_enable(0); tc_enable(0);
_callback_running = true;
} }
static inline void _tc0_stop() { static inline void _tc0_stop() {
// stop the TC0 timer // stop the TC0 timer
tc_disable(0); tc_disable(0);
_callback_running = false;
} }
static void _tc0_initialize() { 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)) { 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(); watch_set_buzzer_off();
_sequence = note_sequence; _sequence = note_sequence;
_cb_finished = callback_on_end; _cb_finished = callback_on_end;
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
_seq_position = 0; _seq_position = 0;
_tone_ticks = 0; _tone_ticks = 0;
_repeat_counter = -1; _repeat_counter = -1;
// prepare buzzer // prepare buzzer
watch_enable_buzzer();
_cb_tc0 = cb_watch_buzzer_seq;
// setup TC0 timer // setup TC0 timer
_tc0_initialize(); _tc0_initialize();
// TCC should run in standby mode
_tcc_write_RUNSTDBY(true);
// start the timer (for the 64 hz callback) // start the timer (for the 64 hz callback)
_tc0_start(); _tc0_start();
} }
@@ -110,51 +135,156 @@ void cb_watch_buzzer_seq(void) {
// read note // read note
watch_buzzer_note_t note = _sequence[_seq_position]; watch_buzzer_note_t note = _sequence[_seq_position];
if (note != BUZZER_NOTE_REST) { 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(); watch_set_buzzer_on();
} else watch_set_buzzer_off(); } else watch_set_buzzer_off();
// set duration ticks and move to next tone // set duration ticks and move to next tone
_tone_ticks = _sequence[_seq_position + 1]; _tone_ticks = _sequence[_seq_position + 1] - 1;
_seq_position += 2; _seq_position += 2;
} else { } else {
// end the sequence // end the sequence
watch_buzzer_abort_sequence(); watch_buzzer_abort_sequence();
if (_cb_finished) _cb_finished();
} }
} else _tone_ticks--; } 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) { void watch_buzzer_abort_sequence(void) {
// ends/aborts the sequence // ends/aborts the sequence
if (_callback_running) _tc0_stop(); if (!_buzzer_is_active) {
return;
}
_tc0_stop();
watch_set_buzzer_off(); 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) { void irq_handler_tc0(void) {
// interrupt handler for TC0 (globally!) // interrupt handler for TC0 (globally!)
cb_watch_buzzer_seq(); if (_cb_tc0) {
_cb_tc0();
}
TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
} }
bool watch_is_buzzer_or_led_enabled(void){ void _watch_maybe_enable_tcc(void) {
return tcc_is_enabled(0); 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) { void _watch_maybe_disable_tcc(void) {
if (!tcc_is_enabled(0)) { if (_buzzer_is_active || _led_is_active) {
_watch_enable_tcc(); 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) { void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) {
tcc_set_period(0, period, true); tcc_set_period(0, period, true);
tcc_set_cc(0, (WATCH_BUZZER_TCC_CHANNEL) % 4, period / (100 / duty), true); tcc_set_cc(0, (WATCH_BUZZER_TCC_CHANNEL) % 4, period / (100 / duty), true);
} // 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.
void watch_disable_buzzer(void) { if (_led_is_active) {
_watch_disable_tcc(); _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) { 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) { 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) { static int8_t single_note_sequence[3];
watch_set_buzzer_off();
} else { single_note_sequence[0] = note;
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25); // 64 ticks per second for the tc0
watch_set_buzzer_on(); // Each tick is approximately 15ms
} uint16_t duration = duration_ms / 15;
delay_ms(duration_ms); if (duration > 127) duration = 127;
watch_set_buzzer_off(); 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) { 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); tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, 0, false);
#endif #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 // Enable the TCC
tcc_enable(0); tcc_enable(0);
} }
@@ -257,13 +375,45 @@ void _watch_disable_tcc(void) {
} }
void watch_enable_leds(void) { void watch_enable_leds(void) {
if (!tcc_is_enabled(0)) { _led_is_active = true;
_watch_enable_tcc(); _watch_enable_led_pins();
} _watch_maybe_enable_tcc();
} }
void watch_disable_leds(void) { 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) { 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 #endif
} }
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue) {
if (tcc_is_enabled(0)) { tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true);
uint32_t period = tcc_get_period(0);
tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true);
#ifdef WATCH_GREEN_TCC_CHANNEL #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 #else
(void) green; // silence warning (void) green; // silence warning
#endif #endif
#ifdef WATCH_BLUE_TCC_CHANNEL #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 #else
(void) blue; // silence warning (void) blue; // silence warning
#endif #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();
} }
} }
+62 -1
View File
@@ -277,20 +277,26 @@ inline void lis2dw_disable_fifo(void) {
#endif #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 #ifdef I2C_SERCOM
uint8_t temp = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_FIFO_SAMPLE); uint8_t temp = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_FIFO_SAMPLE);
bool overrun = !!(temp & LIS2DW_FIFO_SAMPLE_OVERRUN); bool overrun = !!(temp & LIS2DW_FIFO_SAMPLE_OVERRUN);
fifo_data->count = temp & LIS2DW_FIFO_SAMPLE_COUNT; 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++) { 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(); fifo_data->readings[i] = lis2dw_get_raw_reading();
} }
return overrun; return overrun;
#else #else
(void) fifo_data; (void) fifo_data;
(void) timeout;
return false; return false;
#endif #endif
} }
@@ -302,6 +308,20 @@ void lis2dw_clear_fifo(void) {
#endif #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) { void lis2dw_enable_sleep(void) {
#ifdef I2C_SERCOM #ifdef I2C_SERCOM
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); 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 #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) { void lis2dw_enable_interrupts(void) {
#ifdef I2C_SERCOM #ifdef I2C_SERCOM
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7);
@@ -411,6 +458,20 @@ void lis2dw_disable_interrupts(void) {
#endif #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() { lis2dw_wakeup_source_t lis2dw_get_wakeup_source() {
#ifdef I2C_SERCOM #ifdef I2C_SERCOM
return (lis2dw_wakeup_source_t) watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_SRC); return (lis2dw_wakeup_source_t) watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_SRC);
+21 -1
View File
@@ -92,6 +92,12 @@ typedef enum {
LIS2DW_FILTER_HIGH_PASS = 1, LIS2DW_FILTER_HIGH_PASS = 1,
} lis2dw_filter_t; } lis2dw_filter_t;
typedef enum
{
LIS2DW12_INT_PULSED = 0,
LIS2DW12_INT_LATCHED = 1,
} lis2dw12_lir_t;
typedef enum { typedef enum {
LIS2DW_RANGE_16_G = 0b11, // +/- 16g LIS2DW_RANGE_16_G = 0b11, // +/- 16g
LIS2DW_RANGE_8_G = 0b10, // +/- 8g LIS2DW_RANGE_8_G = 0b10, // +/- 8g
@@ -295,6 +301,8 @@ typedef enum {
#define LIS2DW_CTRL7_VAL_HP_REF_MODE 0b00000010 #define LIS2DW_CTRL7_VAL_HP_REF_MODE 0b00000010
#define LIS2DW_CTRL7_VAL_LPASS_ON6D 0b00000001 #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); bool lis2dw_begin(void);
uint8_t lis2dw_get_device_id(void); uint8_t lis2dw_get_device_id(void);
@@ -339,10 +347,14 @@ void lis2dw_enable_fifo(void);
void lis2dw_disable_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_clear_fifo(void);
void lis2dw_enable_double_tap(void);
void lis2dw_disable_double_tap(void);
void lis2dw_enable_sleep(void); void lis2dw_enable_sleep(void);
void lis2dw_disable_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 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_int1(uint8_t sources);
void lis2dw_configure_int2(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_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_interrupt_source_t lis2dw_get_interrupt_source(void);
lis2dw_wakeup_source_t lis2dw_get_wakeup_source(void); lis2dw_wakeup_source_t lis2dw_get_wakeup_source(void);
+98
View File
@@ -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 <stdint.h>
#include <stdbool.h>
/**
* @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);
/** @} */
+6 -3
View File
@@ -45,22 +45,25 @@ void watch_disable_i2c(void);
* @param addr The address of the device you wish to talk to. * @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 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. * @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. /** @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 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 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. * @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. /** @brief Writes a byte to a register in an I2C device.
* @param addr The address of the device you wish to address. * @param addr The address of the device you wish to address.
* @param reg The register on the device that you wish to set. * @param reg The register on the device that you wish to set.
* @param data The value that you wish to set the register to. * @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. /** @brief Reads a byte from a register in an I2C device.
* @param addr The address of the device you wish to address. * @param addr The address of the device you wish to address.
+74 -22
View File
@@ -27,7 +27,7 @@
////< @file watch_rtc.h ////< @file watch_rtc.h
#include "watch.h" #include "watch.h"
#include "rtc.h" #include "rtc32.h"
/** @addtogroup rtc Real-Time Clock /** @addtogroup rtc Real-Time Clock
* @brief This section covers functions related to the SAM L22's real-time clock peripheral, including * @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 btn_alarm_callback;
extern watch_cb_t a2_callback; extern watch_cb_t a2_callback;
extern watch_cb_t a4_callback; extern watch_cb_t a4_callback;
extern watch_cb_t comp_callback;
#define WATCH_RTC_REFERENCE_YEAR (2020) #define WATCH_RTC_REFERENCE_YEAR (2020)
#define watch_date_time_t rtc_date_time_t #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. /** @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. * You may call this function, but outside of app_init, it should always return true.
*/ */
bool _watch_rtc_is_enabled(void); 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. * @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 * @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 * 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); 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. * @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. * @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); 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 /** @brief Set the current UTC date and time using a unix timestamp
* by the provided mask. */
* @param callback The function you wish to have called when the alarm fires. If this value is NULL, the alarm 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. * 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 counter 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. * @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 alarm interrupt is a versatile tool for scheduling events in the future, especially since it can * @details The hardware RTC provides us with single interrupt that fires when the RTC counter matches a target counter COMP0.
* wake the device from all sleep modes. The key to its versatility is the mask parameter. * With a little bit of logic, we can provide multiple active compare callbacks. Every time a comp callback is
* Suppose we set an alarm for midnight, 00:00:00. * registered/disabled/fired we iterate over all the active comp callbacks and set the hardware COMP0 counter
* * if mask is ALARM_MATCH_SS, the alarm will fire every minute when the clock ticks to seconds == 0. * to the next occurring one.
* * with ALARM_MATCH_MMSS, the alarm will once an hour, at the top of each hour. * With this very simple API, movement can implement one-shot timers to turn off the led and determine button longpresses
* * with ALARM_MATCH_HHMMSS, the alarm will fire at midnight every day. * as well as the inactivity timeouts for resigning and sleeping, as well as emulating the top of the minute alarm.
* 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.
*/ */
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. /** @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. /** @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 * @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 * 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 * 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. * 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); void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency);
+4
View File
@@ -98,6 +98,10 @@ typedef enum {
*/ */
void watch_enable_display(void); 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. /** @brief Sets a pixel. Use this to manually set a pixel with a given common and segment number.
* See <a href="segmap.html">segmap.html</a>. * See <a href="segmap.html">segmap.html</a>.
* @param com the common pin, numbered from 0-2. * @param com the common pin, numbered from 0-2.
+67 -32
View File
@@ -126,12 +126,9 @@ typedef enum {
BUZZER_NOTE_REST ///< no sound BUZZER_NOTE_REST ///< no sound
} watch_buzzer_note_t; } watch_buzzer_note_t;
/** @brief Returns true if either the buzzer or the LED driver is enabled. #define WATCH_BUZZER_PERIOD_REST 0
* @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 typedef bool (*watch_buzzer_raw_source_t)(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration);
* or watch_enable_buzzer functions before using these peripherals.
*/
bool watch_is_buzzer_or_led_enabled(void);
/** @addtogroup tcc Buzzer and LED Control (via the TCC peripheral) /** @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 * @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); void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty);
/** @brief Disables the TCC peripheral that drives the buzzer. /** @brief Disables the TCC peripheral that drives the buzzer (if LED not active).
* @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.
*/ */
void watch_disable_buzzer(void); void watch_disable_buzzer(void);
/** @brief Turns the buzzer output on. It will emit a continuous sound at the given frequency. /** @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 * @note The TCC peripheral that drives the buzzer does run in standby mode; if you wish for buzzer
* output to continue, you should prevent your app from going to sleep. * output to continue, you don't need to prevent your app from going to sleep.
*/ */
void watch_set_buzzer_on(void); 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. /** @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 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 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); 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 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 duration_ms The duration of the note.
* @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD * @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); 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)); 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. /** @brief Aborts a playing sequence.
*/ */
void watch_buzzer_abort_sequence(void); void watch_buzzer_abort_sequence(void);
void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop);
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__
void irq_handler_tc0(void); void irq_handler_tc0(void);
#endif #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. * 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. /** @brief Enables the TCC peripheral, which drives the LEDs.
* @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.
*/ */
void watch_enable_leds(void); void watch_enable_leds(void);
/** @brief Disables the LEDs. /** @brief Disables the TCC peripheral that drives the LEDs (if buzzer not active).
* @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.
*/ */
void watch_disable_leds(void); void watch_disable_leds(void);
/** @brief Sets the LED to a custom color by modulating each output's duty cycle. /** @brief Sets the LED to a custom color by modulating each output's duty cycle.
* @param red The red value from 0-255. * @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. * @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); 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 red The red value from 0-255.
* @param green The green value from 0-255. * @param green The green value from 0-255.
* @param blue The blue 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); 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. */ /** @brief Turns both the red and the green LEDs off. */
void watch_set_led_off(void); 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. /// @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]; extern const uint16_t NotePeriods[108];
+6 -1
View File
@@ -205,7 +205,8 @@ uint32_t watch_utility_date_time_to_unix_time(watch_date_time_t date_time, int32
watch_date_time_t watch_utility_date_time_from_unix_time(uint32_t timestamp, int32_t utc_offset) { watch_date_time_t watch_utility_date_time_from_unix_time(uint32_t timestamp, int32_t utc_offset) {
watch_date_time_t retval; watch_date_time_t retval;
retval.reg = 0; retval.reg = 0;
int32_t days, secs; uint32_t secs;
int32_t days;
int32_t remdays, remsecs, remyears; int32_t remdays, remsecs, remyears;
int32_t qc_cycles, c_cycles, q_cycles; int32_t qc_cycles, c_cycles, q_cycles;
int32_t years, months; 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); 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 watch_utility_seconds_to_duration(uint32_t seconds) {
watch_duration_t retval; watch_duration_t retval;
@@ -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); 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. /** @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 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 * @param highside True if the thermistor is connected to VCC and the series resistor is connected
+15 -15
View File
@@ -64,22 +64,22 @@
<h1 style="text-align: center;">Sensor Watch Emulator</h1> <h1 style="text-align: center;">Sensor Watch Emulator</h1>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1271 1311" width="320"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1271 1311" width="320">
<defs> <defs>
<radialGradient id="Dégradé_sans_nom_3" data-name="Dégradé sans nom 3" cx="635" cy="687" fx="260.1751499985994" r="374.83" gradientUnits="userSpaceOnUse"> <radialGradient id="Dégradé_sans_nom_3" data-name="Dégradé sans nom 3"
<stop offset="0.28" stop-color="lime"/> cx="635" cy="687" fx="260.1751499985994" r="374.83" gradientUnits="userSpaceOnUse">
<stop offset="0.46" stop-color="#00b200" stop-opacity="0.7"/> <stop offset="0.28" stop-color="white"/>
<stop offset="0.65" stop-color="#006700" stop-opacity="0.4"/> <stop offset="0.46" stop-color="#ccc" stop-opacity="0.7"/>
<stop offset="0.82" stop-color="#002f00" stop-opacity="0.19"/> <stop offset="0.65" stop-color="#999" stop-opacity="0.4"/>
<stop offset="0.94" stop-color="#000d00" stop-opacity="0.05"/> <stop offset="0.82" stop-color="#666" stop-opacity="0.19"/>
<stop offset="1" stop-opacity="0"/> <stop offset="0.94" stop-color="#333" stop-opacity="0.05"/>
<stop offset="1" stop-color="#000" stop-opacity="0"/>
</radialGradient> </radialGradient>
<filter id="ledcolor"> <filter id="ledcolor">
<feColorMatrix in="SourceGraphic" type="matrix" <feColorMatrix in="SourceGraphic" type="matrix"
values=" 0 0 0 0 0 values="1 0 0 0 0
0 1 0 0 0 0 1 0 0 0
0 0 0 0 0 0 0 1 0 0
0 0 0 1 0 "/> 0 0 0 1 0"/>
</filter>
</filter>
</defs> </defs>
<g id="Calque"> <g id="Calque">
<path id="btn3" d="M1226,777h39.08a5.92,5.92,0,0,1,5.92,5.92v58.16a5.92,5.92,0,0,1-5.92,5.92H1226a0,0,0,0,1,0,0V777A0,0,0,0,1,1226,777Z" style="fill: #999"/> <path id="btn3" d="M1226,777h39.08a5.92,5.92,0,0,1,5.92,5.92v58.16a5.92,5.92,0,0,1-5.92,5.92H1226a0,0,0,0,1,0,0V777A0,0,0,0,1,1226,777Z" style="fill: #999"/>
-4
View File
@@ -1,9 +1,5 @@
#include "watch.h" #include "watch.h"
bool watch_is_buzzer_or_led_enabled(void) {
return false;
}
bool watch_is_usb_enabled(void) { bool watch_is_usb_enabled(void) {
return true; return true;
} }
+9 -3
View File
@@ -28,11 +28,17 @@ void watch_enable_i2c(void) {}
void watch_disable_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) { uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) {
return 0; return 0;
@@ -51,8 +51,6 @@ int _gettimeofday(struct timeval *tv, void *tzvp) {
return 0; return 0;
} }
void _watch_disable_tcc(void) {}
void _watch_enable_usb(void) {} void _watch_enable_usb(void) {}
void watch_disable_TRNG() {} void watch_disable_TRNG() {}
+253 -118
View File
@@ -21,6 +21,8 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
#include <limits.h>
#include <stdbool.h>
#include "watch_rtc.h" #include "watch_rtc.h"
#include "watch_main_loop.h" #include "watch_main_loop.h"
@@ -29,8 +31,29 @@
#include <emscripten.h> #include <emscripten.h>
#include <emscripten/html5.h> #include <emscripten/html5.h>
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 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_interval_id = -1;
static long alarm_timeout_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 a2_callback;
watch_cb_t a4_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) { bool _watch_rtc_is_enabled(void) {
return true; return rtc_enabled;
} }
void _watch_rtc_init(void) { void _watch_rtc_init(void) {
#if EMSCRIPTEN for (uint8_t index = 0; index < 8; ++index) {
// Shifts the timezone so our local time is converted to UTC and set 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({ 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) { date_time.reg = EM_ASM_INT({
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({
const date = new Date(Date.now() + $0); const date = new Date(Date.now() + $0);
return date.getSeconds() | return date.getSeconds() |
(date.getMinutes() << 6) | (date.getMinutes() << 6) |
@@ -82,27 +137,16 @@ watch_date_time_t watch_rtc_get_date_time(void) {
(date.getDate() << 17) | (date.getDate() << 17) |
((date.getMonth() + 1) << 22) | ((date.getMonth() + 1) << 22) |
((date.getFullYear() - 2020) << 26); ((date.getFullYear() - 2020) << 26);
}, time_offset); }, time_zone_offset);
return retval;
}
rtc_date_time_t watch_get_init_date_time(void) {
rtc_date_time_t date_time = {0};
#ifdef BUILD_YEAR #ifdef BUILD_YEAR
date_time.unit.year = BUILD_YEAR; date_time.unit.year = BUILD_YEAR;
#else
date_time.unit.year = 5;
#endif #endif
#ifdef BUILD_MONTH #ifdef BUILD_MONTH
date_time.unit.month = BUILD_MONTH; date_time.unit.month = BUILD_MONTH;
#else
date_time.unit.month = 1;
#endif #endif
#ifdef BUILD_DAY #ifdef BUILD_DAY
date_time.unit.day = BUILD_DAY; date_time.unit.day = BUILD_DAY;
#else
date_time.unit.day = 1;
#endif #endif
#ifdef BUILD_HOUR #ifdef BUILD_HOUR
date_time.unit.hour = 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); watch_rtc_disable_periodic_callback(1);
} }
static void watch_invoke_periodic_callback(void *userData) { static void _watch_schedule_next_tick(void) {
watch_cb_t callback = userData; if (!rtc_enabled) return;
callback();
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(); 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) { 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. // 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); uint8_t per_n = __builtin_clz(tmp);
double interval = 1000.0 / frequency; // in msec tick_callbacks[per_n] = callback;
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);
} }
void watch_rtc_disable_periodic_callback(uint8_t frequency) { void watch_rtc_disable_periodic_callback(uint8_t frequency) {
if (__builtin_popcount(frequency) != 1) return; if (__builtin_popcount(frequency) != 1) return;
uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24); uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24);
if (tick_callbacks[per_n] != -1) { tick_callbacks[per_n] = NULL;
emscripten_clear_interval(tick_callbacks[per_n]);
tick_callbacks[per_n] = -1;
}
} }
void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) { void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) {
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
if (tick_callbacks[i] != -1 && (mask & (1 << i)) != 0) { if (tick_callbacks[i] && (mask & (1 << i)) != 0) {
emscripten_clear_interval(tick_callbacks[i]); tick_callbacks[i] = NULL;
tick_callbacks[i] = -1;
} }
} }
} }
@@ -166,81 +287,95 @@ void watch_rtc_disable_all_periodic_callbacks(void) {
watch_rtc_disable_matching_periodic_callbacks(0xFF); watch_rtc_disable_matching_periodic_callbacks(0xFF);
} }
static void watch_invoke_alarm_interval_callback(void *userData) { void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) {
if (alarm_callback) alarm_callback(); if (index >= WATCH_RTC_N_COMP_CB) {
} return;
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;
} }
double timeout = EM_ASM_DOUBLE({ comp_callbacks[index].counter = counter;
const now = Date.now(); comp_callbacks[index].callback = callback;
const date = new Date(now); comp_callbacks[index].enabled = true;
const hour = ($0 >> 12) & 0x1f; watch_rtc_schedule_next_comp();
const minute = ($0 >> 6) & 0x3f; }
const second = $0 & 0x3f;
if ($1 == 1) { // SS void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) {
if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1); if (index >= WATCH_RTC_N_COMP_CB) {
date.setSeconds(second); return;
} else if ($1 == 2) { // MMSS }
if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1);
if (minute < date.getMinutes()) date.setHours(date.getHours() + 1); comp_callbacks[index].counter = counter;
date.setMinutes(minute, second); comp_callbacks[index].callback = callback;
} else if ($1 == 3) { // HHMMSS comp_callbacks[index].enabled = true;
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); void watch_rtc_disable_comp_callback(uint8_t index) {
date.setHours(hour, minute, second); if (index >= WATCH_RTC_N_COMP_CB) {
} else { return;
throw 'Invalid alarm match mask'; }
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) { if (schedule_any) {
emscripten_clear_interval(alarm_interval_id); scheduled_comp_counter = comp_counter;
alarm_interval_id = -1; } else {
scheduled_comp_counter = curr_counter - 2;
} }
} }
void watch_rtc_enable(bool en) 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) void watch_rtc_freqcorr_write(int16_t value, int16_t sign)
+10 -4
View File
@@ -50,17 +50,23 @@ void watch_enable_display(void) {
_watch_update_indicator_segments(); _watch_update_indicator_segments();
#endif #endif
EM_ASM({
#if defined(FORCE_CUSTOM_LCD_TYPE) #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 #else
document.getElementById("custom").style.display = "none"; EM_ASM({document.getElementById("custom").style.display = "none";});
EM_ASM({document.getElementById("classic").style.display = "";});
#endif #endif
});
watch_clear_display(); 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) { void watch_set_pixel(uint8_t com, uint8_t seg) {
EM_ASM({ EM_ASM({
document.querySelectorAll("[data-com='" + $0 + "'][data-seg='" + $1 + "']") document.querySelectorAll("[data-com='" + $0 + "'][data-seg='" + $1 + "']")
+132 -44
View File
@@ -28,18 +28,23 @@
#include <emscripten.h> #include <emscripten.h>
#include <emscripten/html5.h> #include <emscripten/html5.h>
static bool buzzer_enabled = false; static volatile bool buzzer_enabled = false;
static uint32_t buzzer_period; static uint32_t buzzer_period;
void cb_watch_buzzer_seq(void *userData); void cb_watch_buzzer_seq(void *userData);
void cb_watch_buzzer_raw_source(void *userData);
static uint16_t _seq_position; static uint16_t _seq_position;
static int8_t _tone_ticks, _repeat_counter; 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 int8_t *_sequence;
static watch_buzzer_raw_source_t _raw_source;
static void* _userdata;
static uint8_t _volume;
static void (*_cb_finished)(void); static void (*_cb_finished)(void);
static watch_cb_t _cb_start_global = NULL;
void _watch_enable_tcc(void) {} static watch_cb_t _cb_stop_global = NULL;
static volatile bool _buzzer_is_active = false;
static inline void _em_interval_stop() { static inline void _em_interval_stop() {
emscripten_clear_interval(_em_interval_id); 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)) { 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(); watch_set_buzzer_off();
_buzzer_is_active = true;
if (_cb_start_global) {
_cb_start_global();
}
_sequence = note_sequence; _sequence = note_sequence;
_cb_finished = callback_on_end; _cb_finished = callback_on_end;
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
_seq_position = 0; _seq_position = 0;
_tone_ticks = 0; _tone_ticks = 0;
_repeat_counter = -1; _repeat_counter = -1;
// prepare buzzer
watch_enable_buzzer();
// initiate 64 hz callback // initiate 64 hz callback
_em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL); _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) { if (note == BUZZER_NOTE_REST) {
watch_set_buzzer_off(); watch_set_buzzer_off();
} else { } 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(); watch_set_buzzer_on();
} }
// set duration ticks and move to next tone // set duration ticks and move to next tone
_tone_ticks = _sequence[_seq_position + 1]; _tone_ticks = _sequence[_seq_position + 1] - 1;
_seq_position += 2; _seq_position += 2;
} else { } else {
// end the sequence // end the sequence
watch_buzzer_abort_sequence(); watch_buzzer_abort_sequence();
if (_cb_finished) _cb_finished();
} }
} else _tone_ticks--; } 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) { void watch_buzzer_abort_sequence(void) {
// ends/aborts the sequence // ends/aborts the sequence
if (_em_interval_id) _em_interval_stop(); if (_em_interval_id) _em_interval_stop();
watch_set_buzzer_off(); 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) { void watch_enable_buzzer(void) {
watch_buzzer_abort_sequence();
buzzer_enabled = true; buzzer_enabled = true;
buzzer_period = NotePeriods[BUZZER_NOTE_A4]; buzzer_period = NotePeriods[BUZZER_NOTE_A4];
EM_ASM({ 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) { void watch_disable_buzzer(void) {
buzzer_enabled = false; buzzer_enabled = false;
buzzer_period = NotePeriods[BUZZER_NOTE_A4]; buzzer_period = NotePeriods[BUZZER_NOTE_A4];
EM_ASM({
if (Module['audioContext']) {
Module['audioContext'].close();
Module['audioContext'] = null;
}
});
} }
void watch_set_buzzer_on(void) { 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) { 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) { static int8_t single_note_sequence[3];
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();
}
main_loop_sleep(duration_ms); single_note_sequence[0] = note;
watch_set_buzzer_off(); // 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_enable_leds(void) {}
void watch_disable_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({ 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 filter = document.getElementById("ledcolor");
let color_matrix = filter.children[0].values.baseVal; let color_matrix = filter.children[0].values.baseVal;
color_matrix[1].value = $0 / 255; // red value color_matrix[0].value = $0 / 255; // red
color_matrix[6].value = $1 / 255; // green value color_matrix[6].value = $1 / 255; // green
document.getElementById('light').style.opacity = Math.min(255, $0 + $1) / 255; color_matrix[12].value = $2 / 255; // blue
}, red, green); document.getElementById('light').style.opacity = Math.min(255, $0 + $1 + $2) / 255;
} }, red, green, blue);
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) {
(void) blue;
watch_set_led_color(red, green);
} }
void watch_set_led_red(void) { 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) { 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) { 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) { void watch_set_led_off(void) {
watch_set_led_color(0, 0); watch_set_led_color_rgb(0, 0, 0);
} }