Merge branch 'main' into tide_face
This commit is contained in:
@@ -20,6 +20,9 @@ TINYUSB_CDC=1
|
||||
# Now we're all set to include gossamer's make rules.
|
||||
include $(GOSSAMER_PATH)/make.mk
|
||||
|
||||
# Don't add gossamer's rtc.c since we are using our own rtc32.c
|
||||
SRCS := $(filter-out $(GOSSAMER_PATH)/peripherals/rtc.c,$(SRCS))
|
||||
|
||||
CFLAGS+=-D_POSIX_C_SOURCE=200112L
|
||||
|
||||
define n
|
||||
@@ -136,6 +139,7 @@ INCLUDES += \
|
||||
-I./watch-library/hardware/watch \
|
||||
|
||||
SRCS += \
|
||||
./watch-library/hardware/watch/rtc32.c \
|
||||
./watch-library/hardware/watch/watch.c \
|
||||
./watch-library/hardware/watch/watch_adc.c \
|
||||
./watch-library/hardware/watch/watch_deepsleep.c \
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include "world_clock2_face.h"
|
||||
#include "watch.h"
|
||||
#include "watch_utility.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
static bool refresh_face;
|
||||
|
||||
|
||||
@@ -448,13 +448,13 @@ static void start_reading(accelerometer_data_acquisition_state_t *state) {
|
||||
|
||||
state->records[state->pos++] = record;
|
||||
lis2dw_fifo_t fifo;
|
||||
lis2dw_read_fifo(&fifo); // dump the fifo, this starts a fresh round of data in continue_reading
|
||||
lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT); // dump the fifo, this starts a fresh round of data in continue_reading
|
||||
}
|
||||
|
||||
static void continue_reading(accelerometer_data_acquisition_state_t *state) {
|
||||
printf("Continue reading\n");
|
||||
lis2dw_fifo_t fifo;
|
||||
lis2dw_read_fifo(&fifo);
|
||||
lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT);
|
||||
|
||||
fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it.
|
||||
uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second.
|
||||
|
||||
+719
-270
File diff suppressed because it is too large
Load Diff
+41
-23
@@ -2,6 +2,7 @@
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Joey Castillo
|
||||
* Copyright (c) 2025 Alessandro Genova
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -120,20 +121,43 @@ typedef enum {
|
||||
EVENT_LIGHT_BUTTON_UP, // The light button was pressed for less than half a second, and released.
|
||||
EVENT_LIGHT_LONG_PRESS, // The light button was held for over half a second, but not yet released.
|
||||
EVENT_LIGHT_LONG_UP, // The light button was held for over half a second, and released.
|
||||
EVENT_LIGHT_REALLY_LONG_PRESS, // The light button was held for more than 1.5 second, note yet released.
|
||||
// EVENT_LIGHT_REALLY_LONG_UP, // The light button was held for more than 1.5 second, and released.
|
||||
EVENT_MODE_BUTTON_DOWN, // The mode button has been pressed, but not yet released.
|
||||
EVENT_MODE_BUTTON_UP, // The mode button was pressed for less than half a second, and released.
|
||||
EVENT_MODE_LONG_PRESS, // The mode button was held for over half a second, but not yet released.
|
||||
EVENT_MODE_LONG_UP, // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event.
|
||||
EVENT_MODE_REALLY_LONG_PRESS, // The mode button was held for more than 1.5 second, note yet released.
|
||||
// EVENT_MODE_REALLY_LONG_UP, // The mode button was held for more than 1.5 second, and released.
|
||||
EVENT_ALARM_BUTTON_DOWN, // The alarm button has been pressed, but not yet released.
|
||||
EVENT_ALARM_BUTTON_UP, // The alarm button was pressed for less than half a second, and released.
|
||||
EVENT_ALARM_LONG_PRESS, // The alarm button was held for over half a second, but not yet released.
|
||||
EVENT_ALARM_LONG_UP, // The alarm button was held for over half a second, and released.
|
||||
EVENT_ALARM_REALLY_LONG_PRESS, // The alarm button was held for more than 1.5 second, note yet released.
|
||||
// EVENT_ALARM_REALLY_LONG_UP, // The alarm button was held for more than 1.5 second, and released.
|
||||
|
||||
EVENT_ACCELEROMETER_WAKE, // The accelerometer has detected motion and woken up.
|
||||
EVENT_SINGLE_TAP, // Accelerometer detected a single tap. This event is not yet implemented.
|
||||
EVENT_DOUBLE_TAP, // Accelerometer detected a double tap. This event is not yet implemented.
|
||||
} movement_event_type_t;
|
||||
|
||||
// Each different timeout type will use a different index when invoking watch_rtc_register_comp_callback
|
||||
typedef enum {
|
||||
LIGHT_BUTTON_TIMEOUT = 0, // Light button longpress timeout
|
||||
MODE_BUTTON_TIMEOUT, // Mode button longpress timeout
|
||||
ALARM_BUTTON_TIMEOUT, // Alarm button longpress timeout
|
||||
LED_TIMEOUT, // LED off timeout
|
||||
RESIGN_TIMEOUT, // Resign active face timeout
|
||||
SLEEP_TIMEOUT, // Low-energy begin timeout
|
||||
MINUTE_TIMEOUT, // Top of the Minute timeout
|
||||
} movement_timeout_index_t;
|
||||
|
||||
typedef enum {
|
||||
BUZZER_PRIORITY_BUTTON = 0, // Buzzer priority for button beeps (lowest priority).
|
||||
BUZZER_PRIORITY_SIGNAL, // Buzzer priority for hourly chime (medium priority).
|
||||
BUZZER_PRIORITY_ALARM, // Buzzer priority for hourly chime (highest priority).
|
||||
} movement_buzzer_priority_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t event_type;
|
||||
uint8_t subsecond;
|
||||
@@ -249,37 +273,16 @@ typedef struct {
|
||||
int16_t current_face_idx;
|
||||
int16_t next_face_idx;
|
||||
bool watch_face_changed;
|
||||
bool fast_tick_enabled;
|
||||
int16_t fast_ticks;
|
||||
|
||||
// LED stuff
|
||||
int16_t light_ticks;
|
||||
|
||||
// alarm stuff
|
||||
int16_t alarm_ticks;
|
||||
bool is_buzzing;
|
||||
watch_buzzer_note_t alarm_note;
|
||||
|
||||
// button tracking for long press
|
||||
uint16_t light_down_timestamp;
|
||||
uint16_t mode_down_timestamp;
|
||||
uint16_t alarm_down_timestamp;
|
||||
bool light_on;
|
||||
|
||||
// background task handling
|
||||
bool woke_from_alarm_handler;
|
||||
bool has_scheduled_background_task;
|
||||
bool needs_wake;
|
||||
|
||||
// low energy mode countdown
|
||||
int32_t le_mode_ticks;
|
||||
|
||||
// app resignation countdown (TODO: consolidate with LE countdown?)
|
||||
int16_t timeout_ticks;
|
||||
|
||||
// stuff for subsecond tracking
|
||||
uint8_t tick_frequency;
|
||||
uint8_t last_second;
|
||||
uint8_t subsecond;
|
||||
uint8_t tick_pern;
|
||||
|
||||
// backup register stuff
|
||||
uint8_t next_available_backup_register;
|
||||
@@ -296,6 +299,10 @@ typedef struct {
|
||||
lis2dw_data_rate_t accelerometer_background_rate;
|
||||
// threshold for considering the wearer is in motion
|
||||
uint8_t accelerometer_motion_threshold;
|
||||
|
||||
// signal and alarm volumes
|
||||
watch_buzzer_volume_t signal_volume;
|
||||
watch_buzzer_volume_t alarm_volume;
|
||||
} movement_state_t;
|
||||
|
||||
void movement_move_to_face(uint8_t watch_face_index);
|
||||
@@ -324,9 +331,11 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index);
|
||||
void movement_request_sleep(void);
|
||||
void movement_request_wake(void);
|
||||
|
||||
void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms);
|
||||
void movement_play_signal(void);
|
||||
void movement_play_alarm(void);
|
||||
void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note);
|
||||
void movement_play_sequence(int8_t *note_sequence, movement_buzzer_priority_t priority);
|
||||
|
||||
uint8_t movement_claim_backup_register(void);
|
||||
|
||||
@@ -339,8 +348,11 @@ void movement_set_timezone_index(uint8_t value);
|
||||
watch_date_time_t movement_get_utc_date_time(void);
|
||||
watch_date_time_t movement_get_local_date_time(void);
|
||||
watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index);
|
||||
uint32_t movement_get_utc_timestamp(void);
|
||||
|
||||
void movement_set_utc_date_time(watch_date_time_t date_time);
|
||||
void movement_set_local_date_time(watch_date_time_t date_time);
|
||||
void movement_set_utc_timestamp(uint32_t timestamp);
|
||||
|
||||
bool movement_button_should_sound(void);
|
||||
void movement_set_button_should_sound(bool value);
|
||||
@@ -348,6 +360,12 @@ void movement_set_button_should_sound(bool value);
|
||||
watch_buzzer_volume_t movement_button_volume(void);
|
||||
void movement_set_button_volume(watch_buzzer_volume_t value);
|
||||
|
||||
watch_buzzer_volume_t movement_signal_volume(void);
|
||||
void movement_set_signal_volume(watch_buzzer_volume_t value);
|
||||
|
||||
watch_buzzer_volume_t movement_alarm_volume(void);
|
||||
void movement_set_alarm_volume(watch_buzzer_volume_t value);
|
||||
|
||||
movement_clock_mode_t movement_clock_mode_24h(void);
|
||||
void movement_set_clock_mode_24h(movement_clock_mode_t value);
|
||||
|
||||
|
||||
+11
-3
@@ -32,13 +32,13 @@ const watch_face_t watch_faces[] = {
|
||||
world_clock_face,
|
||||
sunrise_sunset_face,
|
||||
moon_phase_face,
|
||||
stopwatch_face,
|
||||
fast_stopwatch_face,
|
||||
countdown_face,
|
||||
alarm_face,
|
||||
temperature_display_face,
|
||||
voltage_face,
|
||||
settings_face,
|
||||
set_time_face
|
||||
set_time_face,
|
||||
};
|
||||
|
||||
#define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t))
|
||||
@@ -49,7 +49,7 @@ const watch_face_t watch_faces[] = {
|
||||
* Some folks also like to use this to hide the preferences and time set faces from the normal rotation.
|
||||
* If you don't want any faces to be excluded, set this to 0 and a long Mode press will have no effect.
|
||||
*/
|
||||
#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 4)
|
||||
#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 5)
|
||||
|
||||
/* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */
|
||||
#define SIGNAL_TUNE_DEFAULT
|
||||
@@ -68,6 +68,8 @@ const watch_face_t watch_faces[] = {
|
||||
#define MOVEMENT_DEFAULT_BUTTON_SOUND true
|
||||
|
||||
#define MOVEMENT_DEFAULT_BUTTON_VOLUME WATCH_BUZZER_VOLUME_SOFT
|
||||
#define MOVEMENT_DEFAULT_SIGNAL_VOLUME WATCH_BUZZER_VOLUME_LOUD
|
||||
#define MOVEMENT_DEFAULT_ALARM_VOLUME WATCH_BUZZER_VOLUME_LOUD
|
||||
|
||||
/* Set the timeout before switching back to the main watch face
|
||||
* Valid values are:
|
||||
@@ -100,4 +102,10 @@ const watch_face_t watch_faces[] = {
|
||||
*/
|
||||
#define MOVEMENT_DEFAULT_LED_DURATION 1
|
||||
|
||||
/* Optionally debounce button presses (disable by default).
|
||||
* A value of 4 is a good starting point if you have issues
|
||||
* with multiple button presses firing.
|
||||
*/
|
||||
#define MOVEMENT_DEBOUNCE_TICKS 0
|
||||
|
||||
#endif // MOVEMENT_CONFIG_H_
|
||||
|
||||
@@ -73,5 +73,14 @@
|
||||
#include "wareki_face.h"
|
||||
#include "deadline_face.h"
|
||||
#include "wordle_face.h"
|
||||
#include "blackjack_face.h"
|
||||
#include "endless_runner_face.h"
|
||||
#include "higher_lower_game_face.h"
|
||||
#include "lander_face.h"
|
||||
#include "simon_face.h"
|
||||
#include "ping_face.h"
|
||||
#include "rtccount_face.h"
|
||||
#include "tomato_face.h"
|
||||
#include "solar_time_face.h"
|
||||
#include "tide_face.h"
|
||||
// New includes go above this line.
|
||||
|
||||
@@ -22,6 +22,7 @@ SRCS += \
|
||||
./watch-faces/demo/character_set_face.c \
|
||||
./watch-faces/demo/light_sensor_face.c \
|
||||
./watch-faces/demo/peek_memory_face.c \
|
||||
./watch-faces/demo/rtccount_face.c \
|
||||
./watch-faces/sensor/accelerometer_status_face.c \
|
||||
./watch-faces/sensor/temperature_display_face.c \
|
||||
./watch-faces/sensor/temperature_logging_face.c \
|
||||
@@ -48,5 +49,13 @@ SRCS += \
|
||||
./watch-faces/sensor/lis2dw_monitor_face.c \
|
||||
./watch-faces/complication/wareki_face.c \
|
||||
./watch-faces/complication/deadline_face.c \
|
||||
./watch-faces/complication/blackjack_face.c \
|
||||
./watch-faces/complication/endless_runner_face.c \
|
||||
./watch-faces/complication/higher_lower_game_face.c \
|
||||
./watch-faces/complication/lander_face.c \
|
||||
./watch-faces/complication/simon_face.c \
|
||||
./watch-faces/complication/ping_face.c \
|
||||
./watch-faces/complication/tomato_face.c \
|
||||
./watch-faces/clock/solar_time_face.c \
|
||||
./watch-faces/complication/tide_face.c \
|
||||
# New watch faces go above this line.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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, \
|
||||
})
|
||||
@@ -40,9 +40,9 @@ typedef enum {
|
||||
alarm_setting_idx_beeps
|
||||
} alarm_setting_idx_t;
|
||||
|
||||
static const char _dow_strings[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SO", "ED", "1t", "MF", "WN"};
|
||||
static const uint8_t _blink_idx[ALARM_SETTING_STATES] = {2, 0, 4, 6, 8, 9};
|
||||
static const uint8_t _blink_idx2[ALARM_SETTING_STATES] = {3, 1, 5, 7, 8, 9};
|
||||
static const char _dow_strings_classic[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SU", "ED", "1t", "MF", "WN"};
|
||||
static const char _dow_strings_custom[ALARM_DAY_STATES + 1][3] ={ "AL ", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN", "DAY", "1t ", "M-F", "WKD"};
|
||||
static const uint8_t _beeps_blink_idx = 9;
|
||||
static const watch_buzzer_note_t _buzzer_notes[3] = {BUZZER_NOTE_B6, BUZZER_NOTE_C8, BUZZER_NOTE_A8};
|
||||
|
||||
// Volume is indicated by the three segments 5D, 5G and 5A
|
||||
@@ -67,6 +67,10 @@ static void _alarm_set_signal(alarm_state_t *state) {
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
}
|
||||
|
||||
static void _alarm_show_alarm_on_text(alarm_state_t *state) {
|
||||
watch_display_text(WATCH_POSITION_SECONDS, state->alarm[state->alarm_idx].enabled ? "on" : "--");
|
||||
}
|
||||
|
||||
static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) {
|
||||
char buf[12];
|
||||
bool set_leading_zero = movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_024H;
|
||||
@@ -90,18 +94,34 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) {
|
||||
watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
}
|
||||
|
||||
sprintf(buf, set_leading_zero? "%c%c%2d%02d%02d " : "%c%c%2d%2d%02d ",
|
||||
_dow_strings[i][0], _dow_strings[i][1],
|
||||
(state->alarm_idx + 1),
|
||||
h,
|
||||
state->alarm[state->alarm_idx].minute);
|
||||
// blink items if in settings mode
|
||||
if (state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks) {
|
||||
buf[_blink_idx[state->setting_state]] = buf[_blink_idx2[state->setting_state]] = ' ';
|
||||
bool blinking = state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks;
|
||||
if (state->setting_state == alarm_setting_idx_alarm && blinking) {
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
} else {
|
||||
sprintf(buf, "%2d", (state->alarm_idx + 1));
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
|
||||
}
|
||||
if (state->setting_state == alarm_setting_idx_day && blinking) {
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, " ", " ");
|
||||
} else {
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, _dow_strings_custom[i], _dow_strings_classic[i]);
|
||||
}
|
||||
if (state->setting_state == alarm_setting_idx_hour && blinking) {
|
||||
watch_display_text(WATCH_POSITION_HOURS, " ");
|
||||
} else {
|
||||
sprintf(buf, set_leading_zero? "%02d" : "%2d", h);
|
||||
watch_display_text(WATCH_POSITION_HOURS, buf);
|
||||
}
|
||||
if (state->setting_state == alarm_setting_idx_minute && blinking) {
|
||||
watch_display_text(WATCH_POSITION_MINUTES, " ");
|
||||
} else {
|
||||
sprintf(buf, "%02d", state->alarm[state->alarm_idx].minute);
|
||||
watch_display_text(WATCH_POSITION_MINUTES, buf);
|
||||
}
|
||||
watch_display_text(WATCH_POSITION_FULL, buf);
|
||||
|
||||
if (state->is_setting) {
|
||||
watch_display_text(WATCH_POSITION_SECONDS, " ");
|
||||
// draw pitch level indicator
|
||||
if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_pitch)) {
|
||||
for (i = 0; i <= state->alarm[state->alarm_idx].pitch && i < 3; i++)
|
||||
@@ -110,15 +130,18 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) {
|
||||
// draw beep rounds indicator
|
||||
if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_beeps)) {
|
||||
if (state->alarm[state->alarm_idx].beeps == ALARM_MAX_BEEP_ROUNDS - 1)
|
||||
watch_display_character('L', _blink_idx[alarm_setting_idx_beeps]);
|
||||
watch_display_character('L', _beeps_blink_idx);
|
||||
else {
|
||||
if (state->alarm[state->alarm_idx].beeps == 0)
|
||||
watch_display_character('o', _blink_idx[alarm_setting_idx_beeps]);
|
||||
watch_display_character('o', _beeps_blink_idx);
|
||||
else
|
||||
watch_display_character(state->alarm[state->alarm_idx].beeps + 48, _blink_idx[alarm_setting_idx_beeps]);
|
||||
watch_display_character(state->alarm[state->alarm_idx].beeps + 48, _beeps_blink_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
_alarm_show_alarm_on_text(state);
|
||||
}
|
||||
|
||||
// set alarm indicator
|
||||
_alarm_set_signal(state);
|
||||
@@ -179,9 +202,16 @@ static void _alarm_update_alarm_enabled(alarm_state_t *state) {
|
||||
|
||||
static void _alarm_play_short_beep(uint8_t pitch_idx) {
|
||||
// play a short double beep
|
||||
watch_buzzer_play_note(_buzzer_notes[pitch_idx], 50);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_REST, 50);
|
||||
watch_buzzer_play_note(_buzzer_notes[pitch_idx], 70);
|
||||
static int8_t beep_sequence[] = {
|
||||
0, 4,
|
||||
BUZZER_NOTE_REST, 4,
|
||||
0, 6,
|
||||
0
|
||||
};
|
||||
beep_sequence[0] = _buzzer_notes[pitch_idx];
|
||||
beep_sequence[4] = _buzzer_notes[pitch_idx];
|
||||
|
||||
movement_play_sequence(beep_sequence, 0);
|
||||
}
|
||||
|
||||
static void _alarm_indicate_beep(alarm_state_t *state) {
|
||||
@@ -303,6 +333,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) {
|
||||
// revert change of enabled flag and show it briefly
|
||||
state->alarm[state->alarm_idx].enabled ^= 1;
|
||||
_alarm_set_signal(state);
|
||||
_alarm_show_alarm_on_text(state);
|
||||
delay_ms(275);
|
||||
state->alarm_idx = 0;
|
||||
}
|
||||
@@ -413,14 +444,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) {
|
||||
// play alarm
|
||||
if (state->alarm[state->alarm_playing_idx].beeps == 0) {
|
||||
// short beep
|
||||
if (watch_is_buzzer_or_led_enabled()) {
|
||||
_alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch);
|
||||
} else {
|
||||
// enable, play beep and disable buzzer again
|
||||
watch_enable_buzzer();
|
||||
_alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch);
|
||||
watch_disable_buzzer();
|
||||
}
|
||||
} else {
|
||||
// regular alarm beeps
|
||||
movement_play_alarm_beeps((state->alarm[state->alarm_playing_idx].beeps == (ALARM_MAX_BEEP_ROUNDS - 1) ? 20 : state->alarm[state->alarm_playing_idx].beeps),
|
||||
|
||||
@@ -210,7 +210,7 @@ static inline void _display_counts(baby_kicks_state_t *state) {
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, "baby ");
|
||||
watch_clear_colon();
|
||||
} else {
|
||||
char buf[7];
|
||||
char buf[9];
|
||||
|
||||
snprintf(
|
||||
buf,
|
||||
@@ -259,7 +259,7 @@ static void _display_elapsed_minutes(baby_kicks_state_t *state) {
|
||||
* the total elapsed minutes.
|
||||
*/
|
||||
|
||||
char buf[3];
|
||||
char buf[5];
|
||||
uint32_t elapsed_minutes = _elapsed_minutes(state);
|
||||
uint8_t multiple = elapsed_minutes / 30;
|
||||
uint8_t remainder = elapsed_minutes % 30;
|
||||
|
||||
Executable
+472
@@ -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();
|
||||
}
|
||||
}
|
||||
Executable
+96
@@ -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_
|
||||
@@ -23,80 +23,6 @@
|
||||
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
* # Deadline Face
|
||||
*
|
||||
* This is a watch face for tracking deadlines. It draws inspiration from
|
||||
* other watch faces of the project but focuses on keeping track of
|
||||
* deadlines. You can enter and monitor up to four different deadlines by
|
||||
* providing their respective date and time. The face has two modes:
|
||||
* *running mode* and *settings mode*.
|
||||
*
|
||||
* ## Running Mode
|
||||
*
|
||||
* When the watch face is activated, it defaults to running mode. The top
|
||||
* right corner shows the current deadline number, and the main display
|
||||
* presents the time left until the deadline. The format of the display
|
||||
* varies depending on the remaining time.
|
||||
*
|
||||
* - When less than a day is left, the display shows the remaining hours,
|
||||
* minutes, and seconds in the form `HH:MM:SS`.
|
||||
*
|
||||
* - When less than a month is left, the display shows the remaining days
|
||||
* and hours in the form `DD:HH` with the unit `dy` for days.
|
||||
*
|
||||
* - When less than a year is left, the display shows the remaining months
|
||||
* and days in the form `MM:DD` with the unit `mo` for months.
|
||||
*
|
||||
* - When more than a year is left, the years and months are displayed in
|
||||
* the form `YY:MM` with the unit `yr` for years.
|
||||
*
|
||||
* - When a deadline has passed in the last 24 hours, the display shows
|
||||
* `over` to indicate that the deadline has just recently been reached.
|
||||
*
|
||||
* - When no deadline is set for a particular slot, or if a deadline has
|
||||
* already passed by more than 24 hours, `--:--` is displayed.
|
||||
*
|
||||
* The user can navigate in running mode using the following buttons:
|
||||
*
|
||||
* - The *alarm button* moves the next deadline. There are currently four
|
||||
* slots available for deadlines. When the last slot has been reached,
|
||||
* pressing the button moves to the first slot.
|
||||
*
|
||||
* - A *long press* on the *alarm button* activates settings mode and
|
||||
* enables configuring the currently selected deadline.
|
||||
*
|
||||
* - A *long press* on the *light button* activates a deadline alarm. The
|
||||
* bell icon is displayed, and the alarm will ring upon reaching any of
|
||||
* the deadlines set. It is important to note that the watch will not
|
||||
* enter low-energy sleep mode while the alarm is enabled.
|
||||
*
|
||||
*
|
||||
* ## Settings Mode
|
||||
*
|
||||
* In settings mode, the currently selected slot for a deadline can be
|
||||
* configured by providing the date and the time. Like running mode, the
|
||||
* top right corner of the display indicates the current deadline number.
|
||||
* The main display shows the date and, on the next page, the time to be
|
||||
* configured.
|
||||
*
|
||||
* The user can use the following buttons in settings mode.
|
||||
*
|
||||
* - The *light button* navigates through the different date and time
|
||||
* settings, going from year, month, day, hour, to minute. The selected
|
||||
* position is blinking.
|
||||
*
|
||||
* - A *long press* on the light button resets the date and time to the next
|
||||
* day at midnight. This is the default deadline.
|
||||
*
|
||||
* - The *alarm button* increments the currently selected position. A *long
|
||||
* press* on the *alarm button* changes the value faster.
|
||||
*
|
||||
* - The *mode button* exists setting mode and returns to *running mode*.
|
||||
* Here the selected deadline slot can be changed.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.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 */
|
||||
static inline void _beep(beep_type_t beep_type)
|
||||
{
|
||||
static int8_t beep_sequence[] = {
|
||||
0, 4,
|
||||
0, 6,
|
||||
0, 6,
|
||||
0
|
||||
};
|
||||
|
||||
if (!movement_button_should_sound())
|
||||
return;
|
||||
|
||||
switch (beep_type) {
|
||||
case BEEP_BUTTON:
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
|
||||
beep_sequence[0] = BUZZER_NOTE_C7;
|
||||
beep_sequence[2] = 0;
|
||||
break;
|
||||
|
||||
case BEEP_ENABLE:
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G7, 50);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_REST, 75);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C8, 75);
|
||||
beep_sequence[0] = BUZZER_NOTE_G7;
|
||||
beep_sequence[2] = BUZZER_NOTE_REST;
|
||||
beep_sequence[4] = BUZZER_NOTE_C8;
|
||||
break;
|
||||
|
||||
case BEEP_DISABLE:
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C8, 50);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_REST, 75);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G7, 75);
|
||||
beep_sequence[0] = BUZZER_NOTE_C8;
|
||||
beep_sequence[2] = BUZZER_NOTE_REST;
|
||||
beep_sequence[4] = BUZZER_NOTE_G7;
|
||||
break;
|
||||
}
|
||||
|
||||
movement_play_sequence(beep_sequence, 0);
|
||||
}
|
||||
|
||||
/* Change tick frequency */
|
||||
|
||||
@@ -26,6 +26,80 @@
|
||||
#ifndef DEADLINE_FACE_H_
|
||||
#define DEADLINE_FACE_H_
|
||||
|
||||
/*
|
||||
* # Deadline Face
|
||||
*
|
||||
* This is a watch face for tracking deadlines. It draws inspiration from
|
||||
* other watch faces of the project but focuses on keeping track of
|
||||
* deadlines. You can enter and monitor up to four different deadlines by
|
||||
* providing their respective date and time. The face has two modes:
|
||||
* *running mode* and *settings mode*.
|
||||
*
|
||||
* ## Running Mode
|
||||
*
|
||||
* When the watch face is activated, it defaults to running mode. The top
|
||||
* right corner shows the current deadline number, and the main display
|
||||
* presents the time left until the deadline. The format of the display
|
||||
* varies depending on the remaining time.
|
||||
*
|
||||
* - When less than a day is left, the display shows the remaining hours,
|
||||
* minutes, and seconds in the form `HH:MM:SS`.
|
||||
*
|
||||
* - When less than a month is left, the display shows the remaining days
|
||||
* and hours in the form `DD:HH` with the unit `dy` for days.
|
||||
*
|
||||
* - When less than a year is left, the display shows the remaining months
|
||||
* and days in the form `MM:DD` with the unit `mo` for months.
|
||||
*
|
||||
* - When more than a year is left, the years and months are displayed in
|
||||
* the form `YY:MM` with the unit `yr` for years.
|
||||
*
|
||||
* - When a deadline has passed in the last 24 hours, the display shows
|
||||
* `over` to indicate that the deadline has just recently been reached.
|
||||
*
|
||||
* - When no deadline is set for a particular slot, or if a deadline has
|
||||
* already passed by more than 24 hours, `--:--` is displayed.
|
||||
*
|
||||
* The user can navigate in running mode using the following buttons:
|
||||
*
|
||||
* - The *alarm button* moves the next deadline. There are currently four
|
||||
* slots available for deadlines. When the last slot has been reached,
|
||||
* pressing the button moves to the first slot.
|
||||
*
|
||||
* - A *long press* on the *alarm button* activates settings mode and
|
||||
* enables configuring the currently selected deadline.
|
||||
*
|
||||
* - A *long press* on the *light button* activates a deadline alarm. The
|
||||
* bell icon is displayed, and the alarm will ring upon reaching any of
|
||||
* the deadlines set. It is important to note that the watch will not
|
||||
* enter low-energy sleep mode while the alarm is enabled.
|
||||
*
|
||||
*
|
||||
* ## Settings Mode
|
||||
*
|
||||
* In settings mode, the currently selected slot for a deadline can be
|
||||
* configured by providing the date and the time. Like running mode, the
|
||||
* top right corner of the display indicates the current deadline number.
|
||||
* The main display shows the date and, on the next page, the time to be
|
||||
* configured.
|
||||
*
|
||||
* The user can use the following buttons in settings mode.
|
||||
*
|
||||
* - The *light button* navigates through the different date and time
|
||||
* settings, going from year, month, day, hour, to minute. The selected
|
||||
* position is blinking.
|
||||
*
|
||||
* - A *long press* on the light button resets the date and time to the next
|
||||
* day at midnight. This is the default deadline.
|
||||
*
|
||||
* - The *alarm button* increments the currently selected position. A *long
|
||||
* press* on the *alarm button* changes the value faster.
|
||||
*
|
||||
* - The *mode button* exists setting mode and returns to *running mode*.
|
||||
* Here the selected deadline slot can be changed.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/* Modes of face */
|
||||
|
||||
+184
-131
@@ -25,6 +25,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "endless_runner_face.h"
|
||||
#include "delay.h"
|
||||
|
||||
typedef enum {
|
||||
JUMPING_FINAL_FRAME = 0,
|
||||
@@ -34,6 +35,7 @@ typedef enum {
|
||||
|
||||
typedef enum {
|
||||
SCREEN_TITLE = 0,
|
||||
SCREEN_SCORE,
|
||||
SCREEN_PLAYING,
|
||||
SCREEN_LOSE,
|
||||
SCREEN_TIME,
|
||||
@@ -77,14 +79,45 @@ typedef struct {
|
||||
uint8_t fuel;
|
||||
} game_state_t;
|
||||
|
||||
// always-on, left, right, bottom, jump-top, jump-left, jump-right
|
||||
int8_t classic_ball_arr_com[] = {1, 0, 1, 0, 2, 1, 2};
|
||||
int8_t classic_ball_arr_seg[] = {20, 20, 21, 21, 20, 17, 21};
|
||||
int8_t custom_ball_arr_com[] = {2, 1, 1, 0, 3, 3, 2};
|
||||
int8_t custom_ball_arr_seg[] = {15, 15, 14, 15, 14, 15, 14};
|
||||
|
||||
// obstacle 0-11
|
||||
int8_t classic_obstacle_arr_com[] = {0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1};
|
||||
int8_t classic_obstacle_arr_seg[] = {18, 19, 20, 21, 22, 23, 0, 1, 2, 4, 5, 6};
|
||||
int8_t custom_obstacle_arr_com[] = {1, 1, 1, 1, 1, 0, 1, 0, 3, 0, 0, 2};
|
||||
int8_t custom_obstacle_arr_seg[] = {22, 16, 15, 14, 1, 2, 3, 4, 4, 5, 6, 7};
|
||||
|
||||
int8_t *ball_arr_com;
|
||||
int8_t *ball_arr_seg;
|
||||
int8_t *obstacle_arr_com;
|
||||
int8_t *obstacle_arr_seg;
|
||||
|
||||
static game_state_t game_state;
|
||||
static const uint8_t _num_bits_obst_pattern = sizeof(game_state.obst_pattern) * 8;
|
||||
|
||||
int8_t start_tune[] = {
|
||||
BUZZER_NOTE_C5, 15,
|
||||
BUZZER_NOTE_E5, 15,
|
||||
BUZZER_NOTE_G5, 15,
|
||||
0
|
||||
};
|
||||
|
||||
int8_t lose_tune[] = {
|
||||
BUZZER_NOTE_D3, 10,
|
||||
BUZZER_NOTE_C3SHARP_D3FLAT, 10,
|
||||
BUZZER_NOTE_C3, 10,
|
||||
0
|
||||
};
|
||||
|
||||
static void print_binary(uint32_t value, int bits) {
|
||||
#if __EMSCRIPTEN__
|
||||
for (int i = bits - 1; i >= 0; i--) {
|
||||
// Print each bit
|
||||
printf("%lu", (value >> i) & 1);
|
||||
printf("%u", (value >> i) & 1);
|
||||
// Optional: add a space every 4 bits for readability
|
||||
if (i % 4 == 0 && i != 0) {
|
||||
printf(" ");
|
||||
@@ -188,22 +221,22 @@ static uint32_t get_random_legal(uint32_t prev_val, uint16_t difficulty) {
|
||||
|
||||
static void display_ball(bool jumping) {
|
||||
if (!jumping) {
|
||||
watch_set_pixel(0, 21);
|
||||
watch_set_pixel(1, 21);
|
||||
watch_set_pixel(0, 20);
|
||||
watch_set_pixel(1, 20);
|
||||
watch_clear_pixel(1, 17);
|
||||
watch_clear_pixel(2, 20);
|
||||
watch_clear_pixel(2, 21);
|
||||
watch_set_pixel(ball_arr_com[3], ball_arr_seg[3]);
|
||||
watch_set_pixel(ball_arr_com[2], ball_arr_seg[2]);
|
||||
watch_set_pixel(ball_arr_com[1], ball_arr_seg[1]);
|
||||
watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]);
|
||||
watch_clear_pixel(ball_arr_com[6], ball_arr_seg[6]);
|
||||
watch_clear_pixel(ball_arr_com[5], ball_arr_seg[5]);
|
||||
watch_clear_pixel(ball_arr_com[4], ball_arr_seg[4]);
|
||||
}
|
||||
else {
|
||||
watch_clear_pixel(0, 21);
|
||||
watch_clear_pixel(1, 21);
|
||||
watch_clear_pixel(0, 20);
|
||||
watch_set_pixel(1, 20);
|
||||
watch_set_pixel(1, 17);
|
||||
watch_set_pixel(2, 20);
|
||||
watch_set_pixel(2, 21);
|
||||
watch_clear_pixel(ball_arr_com[3], ball_arr_seg[3]);
|
||||
watch_clear_pixel(ball_arr_com[2], ball_arr_seg[2]);
|
||||
watch_clear_pixel(ball_arr_com[1], ball_arr_seg[1]);
|
||||
watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]);
|
||||
watch_set_pixel(ball_arr_com[6], ball_arr_seg[6]);
|
||||
watch_set_pixel(ball_arr_com[5], ball_arr_seg[5]);
|
||||
watch_set_pixel(ball_arr_com[4], ball_arr_seg[4]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,12 +245,12 @@ static void display_score(uint8_t score) {
|
||||
if (game_state.fuel_mode) {
|
||||
score %= (MAX_DISP_SCORE_FUEL + 1);
|
||||
sprintf(buf, "%1d", score);
|
||||
watch_display_string(buf, 0);
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, buf);
|
||||
}
|
||||
else {
|
||||
score %= (MAX_DISP_SCORE + 1);
|
||||
sprintf(buf, "%2d", score);
|
||||
watch_display_string(buf, 2);
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,16 +267,16 @@ static void add_to_score(endless_runner_state_t *state) {
|
||||
static void display_fuel(uint8_t subsecond, uint8_t difficulty) {
|
||||
char buf[4];
|
||||
if (difficulty == DIFF_FUEL_1 && game_state.fuel == 0 && subsecond % (FREQ/2) == 0) {
|
||||
watch_display_string(" ", 2); // Blink the 0 fuel to show it cannot be refilled.
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); // Blink the 0 fuel to show it cannot be refilled.
|
||||
return;
|
||||
}
|
||||
sprintf(buf, "%2d", game_state.fuel);
|
||||
watch_display_string(buf, 2);
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
|
||||
}
|
||||
|
||||
static void check_and_reset_hi_score(endless_runner_state_t *state) {
|
||||
// Resets the hi score at the beginning of each month.
|
||||
watch_date_time_t date_time = watch_rtc_get_date_time();
|
||||
watch_date_time_t date_time = movement_get_local_date_time();
|
||||
if ((state -> year_last_hi_score != date_time.unit.year) ||
|
||||
(state -> month_last_hi_score != date_time.unit.month))
|
||||
{
|
||||
@@ -255,28 +288,15 @@ static void check_and_reset_hi_score(endless_runner_state_t *state) {
|
||||
}
|
||||
|
||||
static void display_difficulty(uint16_t difficulty) {
|
||||
switch (difficulty)
|
||||
{
|
||||
case DIFF_BABY:
|
||||
watch_display_string(" b", 2);
|
||||
break;
|
||||
case DIFF_EASY:
|
||||
watch_display_string(" E", 2);
|
||||
break;
|
||||
case DIFF_HARD:
|
||||
watch_display_string(" H", 2);
|
||||
break;
|
||||
case DIFF_FUEL:
|
||||
watch_display_string(" F", 2);
|
||||
break;
|
||||
case DIFF_FUEL_1:
|
||||
watch_display_string("1F", 2);
|
||||
break;
|
||||
case DIFF_NORM:
|
||||
default:
|
||||
watch_display_string(" N", 2);
|
||||
break;
|
||||
}
|
||||
static const char *labels[] = {
|
||||
[DIFF_BABY] = " b",
|
||||
[DIFF_EASY] = " E",
|
||||
[DIFF_HARD] = " H",
|
||||
[DIFF_FUEL] = " F",
|
||||
[DIFF_FUEL_1] = "1F",
|
||||
[DIFF_NORM] = " N"
|
||||
};
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]);
|
||||
game_state.fuel_mode = difficulty >= DIFF_FUEL && difficulty <= DIFF_FUEL_1;
|
||||
}
|
||||
|
||||
@@ -289,65 +309,93 @@ static void change_difficulty(endless_runner_state_t *state) {
|
||||
}
|
||||
}
|
||||
|
||||
static void toggle_sound(endless_runner_state_t *state) {
|
||||
state -> soundOn = !state -> soundOn;
|
||||
if (state -> soundOn){
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
|
||||
static void display_sound_indicator(bool soundOn) {
|
||||
if (soundOn){
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
}
|
||||
}
|
||||
|
||||
static void toggle_sound(endless_runner_state_t *state) {
|
||||
state -> soundOn = !state -> soundOn;
|
||||
display_sound_indicator(state -> soundOn);
|
||||
if (state -> soundOn){
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
|
||||
}
|
||||
}
|
||||
|
||||
static void enable_tap_control(endless_runner_state_t *state) {
|
||||
if (!state->tap_control_on) {
|
||||
movement_enable_tap_detection_if_available();
|
||||
state->tap_control_on = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void disable_tap_control(endless_runner_state_t *state) {
|
||||
if (state->tap_control_on) {
|
||||
movement_disable_tap_detection_if_available();
|
||||
state->tap_control_on = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void display_title(endless_runner_state_t *state) {
|
||||
game_state.curr_screen = SCREEN_TITLE;
|
||||
watch_clear_colon();
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP, "ENdLS", "ER ");
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, "RUNNER");
|
||||
display_sound_indicator(state -> soundOn);
|
||||
}
|
||||
|
||||
static void display_score_screen(endless_runner_state_t *state) {
|
||||
uint16_t hi_score = state -> hi_score;
|
||||
uint8_t difficulty = state -> difficulty;
|
||||
bool sound_on = state -> soundOn;
|
||||
game_state.curr_screen = SCREEN_TITLE;
|
||||
memset(&game_state, 0, sizeof(game_state));
|
||||
game_state.curr_screen = SCREEN_SCORE;
|
||||
game_state.sec_before_moves = 1; // The first obstacles will all be 0s, which is about an extra second of delay.
|
||||
if (sound_on) game_state.sec_before_moves--; // Start chime is about 1 second
|
||||
watch_set_colon();
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN ", "ER ");
|
||||
if (hi_score > MAX_HI_SCORE) {
|
||||
watch_display_string("ER HS --", 0);
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, "HS --");
|
||||
}
|
||||
else {
|
||||
char buf[14];
|
||||
sprintf(buf, "ER HS%4d", hi_score);
|
||||
watch_display_string(buf, 0);
|
||||
char buf[10];
|
||||
sprintf(buf, "HS%4d", hi_score);
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, buf);
|
||||
}
|
||||
display_difficulty(difficulty);
|
||||
display_sound_indicator(sound_on);
|
||||
}
|
||||
|
||||
static void display_time(watch_date_time_t date_time, bool clock_mode_24h) {
|
||||
static void display_time(void) {
|
||||
static watch_date_time_t previous_date_time;
|
||||
watch_date_time_t date_time = movement_get_local_date_time();
|
||||
movement_clock_mode_t clock_mode_24h = movement_clock_mode_24h();
|
||||
char buf[6 + 1];
|
||||
|
||||
// If the hour needs updating or it's the first time displaying the time
|
||||
if ((game_state.curr_screen != SCREEN_TIME) || (date_time.unit.hour != previous_date_time.unit.hour)) {
|
||||
uint8_t hour = date_time.unit.hour;
|
||||
game_state.curr_screen = SCREEN_TIME;
|
||||
|
||||
if (clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
if (!watch_sleep_animation_is_running()) {
|
||||
watch_set_colon();
|
||||
watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500);
|
||||
}
|
||||
if (clock_mode_24h != MOVEMENT_CLOCK_MODE_12H) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
else {
|
||||
if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM);
|
||||
hour %= 12;
|
||||
if (hour == 0) hour = 12;
|
||||
}
|
||||
watch_set_colon();
|
||||
sprintf( buf, "%2d%02d ", hour, date_time.unit.minute);
|
||||
watch_display_string(buf, 4);
|
||||
sprintf( buf, clock_mode_24h == MOVEMENT_CLOCK_MODE_024H ? "%02d%02d " : "%2d%02d ", hour, date_time.unit.minute);
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, buf);
|
||||
}
|
||||
// If both digits of the minute need updating
|
||||
else if ((date_time.unit.minute / 10) != (previous_date_time.unit.minute / 10)) {
|
||||
sprintf( buf, "%02d ", date_time.unit.minute);
|
||||
watch_display_string(buf, 6);
|
||||
}
|
||||
// If only the ones-place of the minute needs updating.
|
||||
else if (date_time.unit.minute != previous_date_time.unit.minute) {
|
||||
sprintf( buf, "%d ", date_time.unit.minute % 10);
|
||||
watch_display_string(buf, 7);
|
||||
// If only the minute need updating
|
||||
else {
|
||||
sprintf( buf, "%02d", date_time.unit.minute);
|
||||
watch_display_text(WATCH_POSITION_MINUTES, buf);
|
||||
}
|
||||
previous_date_time.reg = date_time.reg;
|
||||
}
|
||||
@@ -356,36 +404,37 @@ static void begin_playing(endless_runner_state_t *state) {
|
||||
uint8_t difficulty = state -> difficulty;
|
||||
game_state.curr_screen = SCREEN_PLAYING;
|
||||
watch_clear_colon();
|
||||
display_sound_indicator(state -> soundOn);
|
||||
movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ);
|
||||
if (game_state.fuel_mode) {
|
||||
watch_display_string(" ", 0);
|
||||
watch_clear_display();
|
||||
game_state.obst_pattern = get_random_fuel(0);
|
||||
if ((16 * JUMP_FRAMES_FUEL_RECHARGE) < JUMP_FRAMES_FUEL) // 16 frames of zeros at the start of a level
|
||||
game_state.fuel = JUMP_FRAMES_FUEL - (16 * JUMP_FRAMES_FUEL_RECHARGE); // Have it below its max to show it counting up when starting.
|
||||
if (game_state.fuel < JUMP_FRAMES_FUEL_RECHARGE) game_state.fuel = JUMP_FRAMES_FUEL_RECHARGE;
|
||||
}
|
||||
else {
|
||||
watch_display_string(" ", 2);
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, " ");
|
||||
game_state.obst_pattern = get_random_legal(0, difficulty);
|
||||
}
|
||||
game_state.jump_state = NOT_JUMPING;
|
||||
display_ball(game_state.jump_state != NOT_JUMPING);
|
||||
display_score( game_state.curr_score);
|
||||
if (state -> soundOn){
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C5, 200);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E5, 200);
|
||||
watch_buzzer_play_note(BUZZER_NOTE_G5, 200);
|
||||
watch_buzzer_play_sequence(start_tune, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void display_lose_screen(endless_runner_state_t *state) {
|
||||
game_state.curr_screen = SCREEN_LOSE;
|
||||
game_state.curr_score = 0;
|
||||
watch_display_string(" LOSE ", 0);
|
||||
if (state -> soundOn)
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A1, 600);
|
||||
else
|
||||
watch_clear_display();
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, " LOSE ");
|
||||
if (state -> soundOn) {
|
||||
watch_buzzer_play_sequence(lose_tune, NULL);
|
||||
delay_ms(600);
|
||||
}
|
||||
}
|
||||
|
||||
static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t *state) {
|
||||
@@ -395,9 +444,9 @@ static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t
|
||||
case 2:
|
||||
game_state.loc_2_on = obstacle;
|
||||
if (obstacle)
|
||||
watch_set_pixel(0, 20);
|
||||
watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
|
||||
else if (game_state.jump_state != NOT_JUMPING) {
|
||||
watch_clear_pixel(0, 20);
|
||||
watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
|
||||
if (game_state.fuel_mode && prev_obst_pos_two)
|
||||
add_to_score(state);
|
||||
}
|
||||
@@ -406,55 +455,20 @@ static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t
|
||||
case 3:
|
||||
game_state.loc_3_on = obstacle;
|
||||
if (obstacle)
|
||||
watch_set_pixel(1, 21);
|
||||
watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
|
||||
else if (game_state.jump_state != NOT_JUMPING)
|
||||
watch_clear_pixel(1, 21);
|
||||
watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
if (!game_state.fuel_mode && obstacle) // If an obstacle is here, it means the ball cleared it
|
||||
add_to_score(state);
|
||||
//fall through
|
||||
case 0:
|
||||
case 5:
|
||||
if (obstacle)
|
||||
watch_set_pixel(0, 18 + grid_loc);
|
||||
else
|
||||
watch_clear_pixel(0, 18 + grid_loc);
|
||||
break;
|
||||
case 4:
|
||||
if (obstacle)
|
||||
watch_set_pixel(1, 22);
|
||||
else
|
||||
watch_clear_pixel(1, 22);
|
||||
break;
|
||||
case 6:
|
||||
if (obstacle)
|
||||
watch_set_pixel(1, 0);
|
||||
else
|
||||
watch_clear_pixel(1, 0);
|
||||
break;
|
||||
case 7:
|
||||
case 8:
|
||||
if (obstacle)
|
||||
watch_set_pixel(0, grid_loc - 6);
|
||||
else
|
||||
watch_clear_pixel(0, grid_loc - 6);
|
||||
break;
|
||||
case 9:
|
||||
case 10:
|
||||
if (obstacle)
|
||||
watch_set_pixel(0, grid_loc - 5);
|
||||
else
|
||||
watch_clear_pixel(0, grid_loc - 5);
|
||||
break;
|
||||
case 11:
|
||||
if (obstacle)
|
||||
watch_set_pixel(1, 6);
|
||||
else
|
||||
watch_clear_pixel(1, 6);
|
||||
break;
|
||||
default:
|
||||
if (obstacle)
|
||||
watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
|
||||
else
|
||||
watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -546,26 +560,37 @@ void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr) {
|
||||
memset(*context_ptr, 0, sizeof(endless_runner_state_t));
|
||||
endless_runner_state_t *state = (endless_runner_state_t *)*context_ptr;
|
||||
state->difficulty = DIFF_NORM;
|
||||
state->tap_control_on = false;
|
||||
}
|
||||
}
|
||||
|
||||
void endless_runner_face_activate(void *context) {
|
||||
(void) context;
|
||||
bool is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM;
|
||||
ball_arr_com = is_custom_lcd ? custom_ball_arr_com : classic_ball_arr_com;
|
||||
ball_arr_seg = is_custom_lcd ? custom_ball_arr_seg : classic_ball_arr_seg;
|
||||
obstacle_arr_com = is_custom_lcd ? custom_obstacle_arr_com : classic_obstacle_arr_com;
|
||||
obstacle_arr_seg = is_custom_lcd ? custom_obstacle_arr_seg : classic_obstacle_arr_seg;
|
||||
if (watch_sleep_animation_is_running()) {
|
||||
watch_stop_blink();
|
||||
}
|
||||
}
|
||||
|
||||
bool endless_runner_face_loop(movement_event_t event, void *context) {
|
||||
endless_runner_state_t *state = (endless_runner_state_t *)context;
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
disable_tap_control(state);
|
||||
check_and_reset_hi_score(state);
|
||||
if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
display_title(state);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
switch (game_state.curr_screen)
|
||||
{
|
||||
case SCREEN_TITLE:
|
||||
case SCREEN_SCORE:
|
||||
case SCREEN_LOSE:
|
||||
case SCREEN_TIME:
|
||||
break;
|
||||
default:
|
||||
update_game(state, event.subsecond);
|
||||
@@ -574,15 +599,37 @@ bool endless_runner_face_loop(movement_event_t event, void *context) {
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
if (game_state.curr_screen == SCREEN_TITLE)
|
||||
switch (game_state.curr_screen) {
|
||||
case SCREEN_SCORE:
|
||||
enable_tap_control(state);
|
||||
begin_playing(state);
|
||||
else if (game_state.curr_screen == SCREEN_LOSE)
|
||||
display_title(state);
|
||||
break;
|
||||
case SCREEN_TITLE:
|
||||
enable_tap_control(state);
|
||||
// fall through
|
||||
case SCREEN_TIME:
|
||||
case SCREEN_LOSE:
|
||||
watch_clear_display();
|
||||
display_score_screen(state);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
if (game_state.curr_screen == SCREEN_TITLE)
|
||||
if (game_state.curr_screen == SCREEN_SCORE)
|
||||
change_difficulty(state);
|
||||
break;
|
||||
case EVENT_SINGLE_TAP:
|
||||
case EVENT_DOUBLE_TAP:
|
||||
if (state->difficulty > DIFF_HARD) break; // Don't do this on fuel modes
|
||||
// Allow starting a new game by tapping.
|
||||
if (game_state.curr_screen == SCREEN_SCORE) {
|
||||
begin_playing(state);
|
||||
break;
|
||||
}
|
||||
else if (game_state.curr_screen == SCREEN_LOSE) {
|
||||
display_score_screen(state);
|
||||
break;
|
||||
}
|
||||
//fall through
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
if (game_state.curr_screen == SCREEN_PLAYING && game_state.jump_state == NOT_JUMPING){
|
||||
@@ -592,15 +639,21 @@ bool endless_runner_face_loop(movement_event_t event, void *context) {
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (game_state.curr_screen != SCREEN_PLAYING)
|
||||
if (game_state.curr_screen == SCREEN_TITLE || game_state.curr_screen == SCREEN_SCORE)
|
||||
toggle_sound(state);
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
if (game_state.curr_screen != SCREEN_TITLE)
|
||||
display_title(state);
|
||||
disable_tap_control(state);
|
||||
if (game_state.curr_screen != SCREEN_SCORE)
|
||||
display_score_screen(state);
|
||||
break;
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
display_time(watch_rtc_get_date_time(), movement_clock_mode_24h());
|
||||
if (game_state.curr_screen != SCREEN_TIME) {
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN ", "ER ");
|
||||
display_sound_indicator(state -> soundOn);
|
||||
display_difficulty(state->difficulty);
|
||||
}
|
||||
display_time();
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event);
|
||||
@@ -609,6 +662,6 @@ bool endless_runner_face_loop(movement_event_t event, void *context) {
|
||||
}
|
||||
|
||||
void endless_runner_face_resign(void *context) {
|
||||
(void) context;
|
||||
endless_runner_state_t *state = (endless_runner_state_t *)context;
|
||||
disable_tap_control(state);
|
||||
}
|
||||
|
||||
+4
-1
@@ -33,6 +33,8 @@
|
||||
This is a basic endless-runner, like the [Chrome Dino game](https://en.wikipedia.org/wiki/Dinosaur_Game).
|
||||
On the title screen, you can select a difficulty by long-pressing LIGHT or toggle sound by long-pressing ALARM.
|
||||
LED or ALARM are used to jump.
|
||||
If the accelerometer is installed, you can tap the screen to jump and move through the menus after using the
|
||||
buttons to go into the first game.
|
||||
High-score is displayed on the top-right on the title screen. During a game, the current score is displayed.
|
||||
*/
|
||||
|
||||
@@ -42,7 +44,8 @@ typedef struct {
|
||||
uint8_t month_last_hi_score : 4;
|
||||
uint8_t year_last_hi_score : 6;
|
||||
uint8_t soundOn : 1;
|
||||
/* 24 bits, likely aligned to 32 bits = 4 bytes */
|
||||
uint8_t tap_control_on : 1;
|
||||
uint8_t unused : 7;
|
||||
} endless_runner_state_t;
|
||||
|
||||
void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr);
|
||||
@@ -2,6 +2,7 @@
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Andreas Nebinger
|
||||
* Copyright (c) 2025 Alessandro Genova
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -24,11 +25,13 @@
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
#include "fast_stopwatch_face.h"
|
||||
#include "watch.h"
|
||||
#include "watch_common_display.h"
|
||||
#include "watch_utility.h"
|
||||
#include "watch_rtc.h"
|
||||
#include "slcd.h"
|
||||
|
||||
/*
|
||||
This watch face implements the original F-91W stopwatch functionality
|
||||
@@ -40,173 +43,247 @@
|
||||
turns on on each button press or it doesn't.
|
||||
*/
|
||||
|
||||
#if __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
#else
|
||||
#include "tc.h"
|
||||
#endif
|
||||
|
||||
// distant future for background task: January 1, 2083
|
||||
static const watch_date_time_t distant_future = {
|
||||
.unit = {0, 0, 0, 1, 1, 63}
|
||||
};
|
||||
|
||||
static uint32_t _ticks;
|
||||
static uint32_t _lap_ticks;
|
||||
static uint8_t _blink_ticks;
|
||||
static uint32_t _old_seconds;
|
||||
static uint8_t _old_minutes;
|
||||
static uint8_t _hours;
|
||||
static bool _colon;
|
||||
static bool _is_running;
|
||||
|
||||
#if __EMSCRIPTEN__
|
||||
|
||||
static long _em_interval_id = 0;
|
||||
|
||||
void em_cb_handler(void *userData) {
|
||||
// interrupt handler for emscripten 128 Hz callbacks
|
||||
(void) userData;
|
||||
_ticks++;
|
||||
}
|
||||
|
||||
static void _cb_initialize() { }
|
||||
|
||||
static inline void _cb_stop() {
|
||||
emscripten_clear_interval(_em_interval_id);
|
||||
_em_interval_id = 0;
|
||||
_is_running = false;
|
||||
}
|
||||
|
||||
static inline void _cb_start() {
|
||||
// initiate 128 hz callback
|
||||
_em_interval_id = emscripten_set_interval(em_cb_handler, (double)(1000/128), (void *)NULL);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static inline void _cb_start() {
|
||||
// start the TC1 timer
|
||||
tc_enable(1);
|
||||
_is_running = true;
|
||||
}
|
||||
|
||||
static inline void _cb_stop() {
|
||||
// stop the TC1 timer
|
||||
tc_disable(1);
|
||||
_is_running = false;
|
||||
}
|
||||
|
||||
static void _cb_initialize() {
|
||||
tc_init(1, GENERIC_CLOCK_3, TC_PRESCALER_DIV4);
|
||||
tc_set_counter_mode(1, TC_COUNTER_MODE_8BIT);
|
||||
tc_set_run_in_standby(1, true);
|
||||
_cb_stop();
|
||||
tc_count8_set_period(1, 1); // 1024 Hz divided by 4 divided by 2 results in a 128 Hz interrupt
|
||||
/// FIXME: #SecondMovement, we need a gossamer wrapper for interrupts.
|
||||
TC1->COUNT8.INTENSET.bit.OVF = 1;
|
||||
NVIC_ClearPendingIRQ(TC1_IRQn);
|
||||
NVIC_EnableIRQ (TC1_IRQn);
|
||||
}
|
||||
|
||||
void irq_handler_tc1(void);
|
||||
void irq_handler_tc1(void) {
|
||||
// interrupt handler for TC1 (globally!)
|
||||
_ticks++;
|
||||
TC1->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
|
||||
}
|
||||
|
||||
#endif
|
||||
// Loosely implement the watch as a state machine
|
||||
typedef enum {
|
||||
SW_STATUS_IDLE = 0,
|
||||
SW_STATUS_RUNNING,
|
||||
SW_STATUS_RUNNING_LAPPING,
|
||||
SW_STATUS_STOPPED,
|
||||
SW_STATUS_STOPPED_LAPPING
|
||||
} stopwatch_status_t;
|
||||
|
||||
static inline void _button_beep() {
|
||||
// play a beep as confirmation for a button press (if applicable)
|
||||
if (movement_button_should_sound()) watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume());
|
||||
}
|
||||
|
||||
// How quickly should the elapsing time be displayed?
|
||||
// This is just for looks, timekeeping is always accurate to 128Hz
|
||||
static const uint8_t DISPLAY_RUNNING_RATE = 32;
|
||||
static const uint8_t DISPLAY_RUNNING_RATE_SLOW = 2;
|
||||
|
||||
/// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter
|
||||
/// on the lcd.
|
||||
/// @param ticks
|
||||
static void _display_ticks(uint32_t ticks) {
|
||||
char buf[14];
|
||||
static void _display_elapsed(fast_stopwatch_state_t *state, uint32_t ticks) {
|
||||
char buf[3];
|
||||
|
||||
if (state->slow_refresh && (state->status == SW_STATUS_RUNNING || state->status == SW_STATUS_IDLE)) {
|
||||
watch_display_character_lp_seconds(' ', 8);
|
||||
watch_display_character_lp_seconds(' ', 9);
|
||||
} else {
|
||||
uint8_t sec_100 = (ticks & 0x7F) * 100 / 128;
|
||||
|
||||
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
|
||||
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
|
||||
}
|
||||
|
||||
uint32_t seconds = ticks >> 7;
|
||||
uint32_t minutes = seconds / 60;
|
||||
if (_hours) {
|
||||
sprintf(buf, "%2u", _hours);
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
|
||||
} else {
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
|
||||
if (seconds == state->old_display.seconds) {
|
||||
return;
|
||||
}
|
||||
|
||||
sprintf(buf, "%02lu%02lu%02u", minutes, (seconds % 60), sec_100);
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, buf);
|
||||
}
|
||||
state->old_display.seconds = seconds;
|
||||
|
||||
/// @brief Displays the current stopwatch time on the LCD (more optimized than _display_ticks())
|
||||
static void _draw() {
|
||||
if (_lap_ticks == 0) {
|
||||
char buf[14];
|
||||
uint8_t sec_100 = (_ticks & 0x7F) * 100 / 128;
|
||||
if (_is_running) {
|
||||
uint32_t seconds = _ticks >> 7;
|
||||
if (seconds != _old_seconds) {
|
||||
// seconds have changed
|
||||
_old_seconds = seconds;
|
||||
uint8_t minutes = seconds / 60;
|
||||
seconds %= 60;
|
||||
if (minutes != _old_minutes) {
|
||||
// minutes have changed, draw everything
|
||||
_old_minutes = minutes;
|
||||
minutes %= 60;
|
||||
if (_hours) {
|
||||
// with hour indicator
|
||||
sprintf(buf, "%2u", _hours);
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
|
||||
} else {
|
||||
// no hour indicator
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
}
|
||||
sprintf(buf, "%02u%02lu%02u", minutes, seconds, sec_100);
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, buf);
|
||||
} else {
|
||||
// just draw seconds
|
||||
sprintf(buf, "%02lu", seconds);
|
||||
// note that we're drawing the seconds in the "minutes" position, since this
|
||||
// watch face uses the "seconds" position for hundredths of seconds
|
||||
sprintf(buf, "%02lu", seconds % 60);
|
||||
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);
|
||||
|
||||
uint32_t minutes = seconds / 60;
|
||||
|
||||
if (minutes == state->old_display.minutes) {
|
||||
return;
|
||||
}
|
||||
|
||||
state->old_display.minutes = minutes;
|
||||
|
||||
sprintf(buf, "%02lu", minutes % 60);
|
||||
watch_display_text(WATCH_POSITION_HOURS, buf);
|
||||
|
||||
uint32_t hours = (minutes / 60) % 24;
|
||||
|
||||
if (hours == state->old_display.hours) {
|
||||
return;
|
||||
}
|
||||
|
||||
state->old_display.hours = hours;
|
||||
|
||||
if (hours) {
|
||||
sprintf(buf, "%2lu", hours);
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
|
||||
} else {
|
||||
// only draw 100ths of seconds
|
||||
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
|
||||
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
|
||||
}
|
||||
} else {
|
||||
_display_ticks(_ticks);
|
||||
}
|
||||
}
|
||||
if (_is_running) {
|
||||
// blink the colon every half second
|
||||
uint8_t blink_ticks = ((_ticks >> 6) & 1);
|
||||
if (blink_ticks != _blink_ticks) {
|
||||
_blink_ticks = blink_ticks;
|
||||
_colon = !_colon;
|
||||
if (_colon) watch_set_colon();
|
||||
else watch_clear_colon();
|
||||
}
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
}
|
||||
}
|
||||
|
||||
static inline void _update_lap_indicator() {
|
||||
if (_lap_ticks) watch_set_indicator(WATCH_INDICATOR_LAP);
|
||||
else watch_clear_indicator(WATCH_INDICATOR_LAP);
|
||||
}
|
||||
static void _draw_indicators(fast_stopwatch_state_t *state, movement_event_t event, uint32_t elapsed) {
|
||||
uint8_t subsecond;
|
||||
bool tock;
|
||||
|
||||
static inline void _set_colon() {
|
||||
switch (state->status) {
|
||||
case SW_STATUS_RUNNING:
|
||||
subsecond = elapsed & 127;
|
||||
tock = subsecond >= 64;
|
||||
|
||||
watch_clear_indicator(WATCH_INDICATOR_LAP);
|
||||
if (tock) {
|
||||
watch_clear_colon();
|
||||
} else {
|
||||
watch_set_colon();
|
||||
_colon = true;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
case SW_STATUS_RUNNING_LAPPING:
|
||||
tock = event.subsecond > 0;
|
||||
|
||||
if (tock) {
|
||||
watch_clear_indicator(WATCH_INDICATOR_LAP);
|
||||
watch_clear_colon();
|
||||
} else {
|
||||
watch_set_indicator(WATCH_INDICATOR_LAP);
|
||||
watch_set_colon();
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
case SW_STATUS_STOPPED_LAPPING:
|
||||
watch_set_indicator(WATCH_INDICATOR_LAP);
|
||||
watch_set_colon();
|
||||
|
||||
return;
|
||||
|
||||
case SW_STATUS_STOPPED:
|
||||
case SW_STATUS_IDLE:
|
||||
default:
|
||||
watch_clear_indicator(WATCH_INDICATOR_LAP);
|
||||
watch_set_colon();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t get_refresh_rate(fast_stopwatch_state_t *state) {
|
||||
switch (state->status) {
|
||||
case SW_STATUS_RUNNING:
|
||||
if (state->slow_refresh) {
|
||||
return DISPLAY_RUNNING_RATE_SLOW;
|
||||
} else {
|
||||
return DISPLAY_RUNNING_RATE;
|
||||
}
|
||||
case SW_STATUS_RUNNING_LAPPING:
|
||||
return 2;
|
||||
case SW_STATUS_STOPPED:
|
||||
case SW_STATUS_IDLE:
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counter, movement_event_type_t event_type) {
|
||||
switch (state->status) {
|
||||
case SW_STATUS_IDLE:
|
||||
switch (event_type) {
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->status = SW_STATUS_RUNNING;
|
||||
state->start_counter = counter;
|
||||
movement_request_tick_frequency(get_refresh_rate(state));
|
||||
return;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
state->slow_refresh = !state->slow_refresh;
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
case SW_STATUS_RUNNING:
|
||||
switch (event_type) {
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->status = SW_STATUS_STOPPED;
|
||||
state->stop_counter = counter;
|
||||
movement_request_tick_frequency(get_refresh_rate(state));
|
||||
return;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
state->status = SW_STATUS_RUNNING_LAPPING;
|
||||
state->lap_counter = counter;
|
||||
movement_request_tick_frequency(get_refresh_rate(state));
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
case SW_STATUS_RUNNING_LAPPING:
|
||||
switch (event_type) {
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->status = SW_STATUS_STOPPED_LAPPING;
|
||||
state->stop_counter = counter;
|
||||
movement_request_tick_frequency(get_refresh_rate(state));
|
||||
return;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
state->status = SW_STATUS_RUNNING;
|
||||
state->lap_counter = counter;
|
||||
movement_request_tick_frequency(get_refresh_rate(state));
|
||||
return;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
state->status = SW_STATUS_RUNNING;
|
||||
state->slow_refresh = !state->slow_refresh;
|
||||
movement_request_tick_frequency(get_refresh_rate(state));
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
case SW_STATUS_STOPPED_LAPPING:
|
||||
switch (event_type) {
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->status = SW_STATUS_RUNNING_LAPPING;
|
||||
state->start_counter = counter - state->stop_counter + state->start_counter;
|
||||
state->lap_counter = counter - state->stop_counter + state->lap_counter;
|
||||
movement_request_tick_frequency(get_refresh_rate(state));
|
||||
return;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
state->status = SW_STATUS_STOPPED;
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
case SW_STATUS_STOPPED:
|
||||
switch (event_type) {
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->status = SW_STATUS_RUNNING;
|
||||
state->start_counter = counter - state->stop_counter + state->start_counter;
|
||||
movement_request_tick_frequency(get_refresh_rate(state));
|
||||
return;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
state->status = SW_STATUS_IDLE;
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t elapsed_time(fast_stopwatch_state_t *state, rtc_counter_t counter) {
|
||||
switch (state->status) {
|
||||
case SW_STATUS_IDLE:
|
||||
return 0;
|
||||
|
||||
case SW_STATUS_RUNNING:
|
||||
return counter - state->start_counter;
|
||||
|
||||
case SW_STATUS_RUNNING_LAPPING:
|
||||
case SW_STATUS_STOPPED_LAPPING:
|
||||
return state->lap_counter - state->start_counter;
|
||||
|
||||
case SW_STATUS_STOPPED:
|
||||
return state->stop_counter - state->start_counter;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
|
||||
@@ -215,114 +292,54 @@ void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
|
||||
*context_ptr = malloc(sizeof(fast_stopwatch_state_t));
|
||||
memset(*context_ptr, 0, sizeof(fast_stopwatch_state_t));
|
||||
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)*context_ptr;
|
||||
_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0;
|
||||
_is_running = _colon = false;
|
||||
state->light_on_button = true;
|
||||
}
|
||||
if (!_is_running) {
|
||||
// prepare the 128 Hz callback source
|
||||
_cb_initialize();
|
||||
state->start_counter = 0;
|
||||
state->stop_counter = 0;
|
||||
state->lap_counter = 0;
|
||||
state->status = SW_STATUS_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
void fast_stopwatch_face_activate(void *context) {
|
||||
(void) context;
|
||||
if (_is_running) {
|
||||
// The background task will keep the watch from entering low energy mode while the stopwatch is on screen.
|
||||
movement_schedule_background_task(distant_future);
|
||||
}
|
||||
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *) context;
|
||||
// force full re-draw
|
||||
state->old_display.seconds = UINT_MAX;
|
||||
state->old_display.minutes = UINT_MAX;
|
||||
state->old_display.hours = UINT_MAX;
|
||||
movement_request_tick_frequency(get_refresh_rate(state));
|
||||
}
|
||||
|
||||
bool fast_stopwatch_face_loop(movement_event_t event, void *context) {
|
||||
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)context;
|
||||
|
||||
// handle overflow of fast ticks
|
||||
while (_ticks >= (128 * 60 * 60)) {
|
||||
_ticks -= (128 * 60 * 60);
|
||||
_hours++;
|
||||
if (_hours >= 24) _hours -= 24;
|
||||
// initiate a re-draw
|
||||
_old_minutes = 59;
|
||||
}
|
||||
rtc_counter_t counter = watch_rtc_get_counter();
|
||||
|
||||
state_transition(state, counter, event.event_type);
|
||||
rtc_counter_t elapsed = elapsed_time(state, counter);
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
_set_colon();
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST");
|
||||
_update_lap_indicator();
|
||||
if (_is_running) movement_request_tick_frequency(16);
|
||||
_display_ticks(_lap_ticks ? _lap_ticks : _ticks);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
_draw();
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
// kind od hidden feature: long press toggles light on or off
|
||||
state->light_on_button = !state->light_on_button;
|
||||
if (state->light_on_button) movement_illuminate_led();
|
||||
else watch_set_led_off();
|
||||
_draw_indicators(state, event, elapsed);
|
||||
_display_elapsed(state, elapsed);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
_is_running = !_is_running;
|
||||
if (_is_running) {
|
||||
// start or continue stopwatch
|
||||
movement_request_tick_frequency(16);
|
||||
// register 128 hz callback for time measuring
|
||||
_cb_start();
|
||||
// schedule the keepalive task when running
|
||||
movement_schedule_background_task(distant_future);
|
||||
} else {
|
||||
// stop the stopwatch
|
||||
_cb_stop();
|
||||
movement_request_tick_frequency(1);
|
||||
_set_colon();
|
||||
// cancel the keepalive task
|
||||
movement_cancel_background_task();
|
||||
}
|
||||
_draw();
|
||||
_button_beep();
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
if (state->light_on_button) movement_illuminate_led();
|
||||
if (_is_running) {
|
||||
if (_lap_ticks) {
|
||||
// clear lap and continue running
|
||||
_lap_ticks = 0;
|
||||
movement_request_tick_frequency(16);
|
||||
} else {
|
||||
// set lap ticks and stop updating the display
|
||||
_lap_ticks = _ticks;
|
||||
movement_request_tick_frequency(2);
|
||||
_set_colon();
|
||||
}
|
||||
} else {
|
||||
if (_lap_ticks) {
|
||||
// clear lap and show running stopwatch
|
||||
_lap_ticks = 0;
|
||||
} else if (_ticks) {
|
||||
// reset stopwatch
|
||||
_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
_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();
|
||||
// fall through
|
||||
case EVENT_TICK:
|
||||
_draw_indicators(state, event, elapsed);
|
||||
_display_elapsed(state, elapsed);
|
||||
break;
|
||||
default:
|
||||
movement_default_loop_handler(event);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void fast_stopwatch_face_resign(void *context) {
|
||||
(void) context;
|
||||
// cancel the keepalive task
|
||||
movement_cancel_background_task();
|
||||
movement_request_tick_frequency(1);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,16 @@
|
||||
#include "movement.h"
|
||||
|
||||
typedef struct {
|
||||
bool light_on_button; // determines whether the light button actually triggers the led
|
||||
rtc_counter_t start_counter; // rtc counter when the stopwatch was started
|
||||
rtc_counter_t lap_counter; // rtc counter when the stopwatch was lapped
|
||||
rtc_counter_t stop_counter; // rtc counter when the stopwatch was stopped
|
||||
uint8_t status; // the status the stopwatch is in (idle, running, stopped)
|
||||
bool slow_refresh; // update the display slowly (same 128Hz timekeeping accuracy)
|
||||
struct {
|
||||
rtc_counter_t seconds;
|
||||
rtc_counter_t minutes;
|
||||
rtc_counter_t hours;
|
||||
} old_display; // the digits currently being displayed on screen
|
||||
} fast_stopwatch_state_t;
|
||||
|
||||
void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr);
|
||||
@@ -63,12 +72,6 @@ void fast_stopwatch_face_activate(void *context);
|
||||
bool fast_stopwatch_face_loop(movement_event_t event, void *context);
|
||||
void fast_stopwatch_face_resign(void *context);
|
||||
|
||||
#if __EMSCRIPTEN__
|
||||
void em_cb_handler(void *userData);
|
||||
#else
|
||||
void TC2_Handler(void);
|
||||
#endif
|
||||
|
||||
#define fast_stopwatch_face ((const watch_face_t){ \
|
||||
fast_stopwatch_face_setup, \
|
||||
fast_stopwatch_face_activate, \
|
||||
|
||||
+45
-31
@@ -30,19 +30,22 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "higher_lower_game_face.h"
|
||||
#include "watch_private_display.h"
|
||||
#include "watch_common_display.h"
|
||||
|
||||
|
||||
#define KING 12
|
||||
#define QUEEN 11
|
||||
#define JACK 10
|
||||
|
||||
#define TITLE_TEXT "Hi-Lo"
|
||||
#define GAME_BOARD_SIZE 6
|
||||
#define MAX_BOARDS 40
|
||||
#define GUESSES_PER_SCREEN 5
|
||||
#define WIN_SCORE (MAX_BOARDS * GUESSES_PER_SCREEN)
|
||||
#define STATUS_DISPLAY_START 0
|
||||
#define BOARD_SCORE_DISPLAY_START 2
|
||||
#define BOARD_DISPLAY_START 4
|
||||
#define BOARD_DISPLAY_END 9
|
||||
#define MIN_CARD_VALUE 2
|
||||
#define MAX_CARD_VALUE 14
|
||||
#define MAX_CARD_VALUE KING
|
||||
#define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1)
|
||||
#define CARD_SUIT_COUNT 4
|
||||
#define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT)
|
||||
@@ -111,7 +114,6 @@ static void shuffle_deck(void) {
|
||||
|
||||
static void reset_deck(void) {
|
||||
current_card = 0;
|
||||
stack_deck();
|
||||
shuffle_deck();
|
||||
}
|
||||
|
||||
@@ -141,8 +143,8 @@ static void reset_board(bool first_round) {
|
||||
|
||||
static void init_game(void) {
|
||||
watch_clear_display();
|
||||
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);
|
||||
watch_display_string("GA", STATUS_DISPLAY_START);
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
|
||||
reset_deck();
|
||||
reset_board(true);
|
||||
score = 0;
|
||||
@@ -151,16 +153,23 @@ static void init_game(void) {
|
||||
}
|
||||
|
||||
static void set_segment_at_position(segment_t segment, uint8_t position) {
|
||||
const uint64_t position_segment_data = (Segment_Map[position] >> (8 * (uint8_t) segment)) & 0xFF;
|
||||
const uint8_t com_pin = position_segment_data >> 6;
|
||||
const uint8_t seg = position_segment_data & 0x3F;
|
||||
digit_mapping_t segmap;
|
||||
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) {
|
||||
segmap = Custom_LCD_Display_Mapping[position];
|
||||
} else {
|
||||
segmap = Classic_LCD_Display_Mapping[position];
|
||||
}
|
||||
const uint8_t com_pin = segmap.segment[segment].address.com;
|
||||
const uint8_t seg = segmap.segment[segment].address.seg;
|
||||
watch_set_pixel(com_pin, seg);
|
||||
}
|
||||
|
||||
static inline size_t get_display_position(size_t board_position) {
|
||||
return FLIP_BOARD_DIRECTION ? BOARD_DISPLAY_START + board_position : BOARD_DISPLAY_END - board_position;
|
||||
}
|
||||
|
||||
static void render_board_position(size_t board_position) {
|
||||
const size_t display_position = FLIP_BOARD_DIRECTION
|
||||
? BOARD_DISPLAY_START + board_position
|
||||
: BOARD_DISPLAY_END - board_position;
|
||||
const size_t display_position = get_display_position(board_position);
|
||||
const bool revealed = game_board[board_position].revealed;
|
||||
|
||||
//// Current position indicator spot
|
||||
@@ -178,18 +187,18 @@ static void render_board_position(size_t board_position) {
|
||||
|
||||
const uint8_t value = game_board[board_position].value;
|
||||
switch (value) {
|
||||
case 14: // A (≡)
|
||||
case KING: // K (≡)
|
||||
watch_display_character(' ', display_position);
|
||||
set_segment_at_position(A, display_position);
|
||||
set_segment_at_position(D, display_position);
|
||||
set_segment_at_position(G, display_position);
|
||||
break;
|
||||
case 13: // K (=)
|
||||
case QUEEN: // Q (=)
|
||||
watch_display_character(' ', display_position);
|
||||
set_segment_at_position(A, display_position);
|
||||
set_segment_at_position(D, display_position);
|
||||
break;
|
||||
case 12: // Q (-)
|
||||
case JACK: // J (-)
|
||||
watch_display_character('-', display_position);
|
||||
break;
|
||||
default: {
|
||||
@@ -209,16 +218,16 @@ static void render_board_count(void) {
|
||||
// Render completed boards (screens)
|
||||
char buf[3] = {0};
|
||||
snprintf(buf, sizeof(buf), "%2hhu", completed_board_count);
|
||||
watch_display_string(buf, BOARD_SCORE_DISPLAY_START);
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
|
||||
}
|
||||
|
||||
static void render_final_score(void) {
|
||||
watch_display_string("SC", STATUS_DISPLAY_START);
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP, "SCORE", "SC ");
|
||||
char buf[7] = {0};
|
||||
const uint8_t complete_boards = score / GUESSES_PER_SCREEN;
|
||||
snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score);
|
||||
watch_set_colon();
|
||||
watch_display_string(buf, BOARD_DISPLAY_START);
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, buf);
|
||||
}
|
||||
|
||||
static guess_t get_answer(void) {
|
||||
@@ -251,13 +260,13 @@ static void do_game_loop(guess_t user_guess) {
|
||||
// Render answer indicator
|
||||
switch (answer) {
|
||||
case HL_GUESS_EQUAL:
|
||||
watch_display_string("==", STATUS_DISPLAY_START);
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "==");
|
||||
break;
|
||||
case HL_GUESS_HIGHER:
|
||||
watch_display_string("HI", STATUS_DISPLAY_START);
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "HI");
|
||||
break;
|
||||
case HL_GUESS_LOWER:
|
||||
watch_display_string("LO", STATUS_DISPLAY_START);
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "LO");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -268,18 +277,22 @@ static void do_game_loop(guess_t user_guess) {
|
||||
// No score for two consecutive identical cards
|
||||
} else {
|
||||
// Incorrect guess, game over
|
||||
watch_display_string("GO", STATUS_DISPLAY_START);
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "End", "GO");
|
||||
game_board[guess_position].revealed = true;
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, "------");
|
||||
render_board_position(guess_position - 1);
|
||||
render_board_position(guess_position);
|
||||
if (game_board[guess_position].value == JACK && guess_position < GAME_BOARD_SIZE) // Adds a space in case the revealed option is '-'
|
||||
watch_display_character(' ', get_display_position(guess_position + 1));
|
||||
game_state = HL_GS_LOSE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (score >= WIN_SCORE) {
|
||||
// Win, perhaps some kind of animation sequence?
|
||||
watch_display_string("WI", STATUS_DISPLAY_START);
|
||||
watch_display_string(" ", BOARD_SCORE_DISPLAY_START);
|
||||
watch_display_string("------", BOARD_DISPLAY_START);
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "WI");
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WINNER", "winnEr");
|
||||
game_state = HL_GS_WIN;
|
||||
return;
|
||||
}
|
||||
@@ -309,12 +322,12 @@ static void do_game_loop(guess_t user_guess) {
|
||||
break;
|
||||
case HL_GS_SHOW_SCORE:
|
||||
watch_clear_display();
|
||||
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);
|
||||
watch_display_string("GA", STATUS_DISPLAY_START);
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
|
||||
game_state = HL_GS_TITLE_SCREEN;
|
||||
break;
|
||||
default:
|
||||
watch_display_string("ERROR", BOARD_DISPLAY_START);
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, "ERROR");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -344,6 +357,7 @@ void higher_lower_game_face_activate(void *context) {
|
||||
(void) state;
|
||||
// Handle any tasks related to your watch face coming on screen.
|
||||
game_state = HL_GS_TITLE_SCREEN;
|
||||
stack_deck();
|
||||
}
|
||||
|
||||
bool higher_lower_game_face_loop(movement_event_t event, void *context) {
|
||||
@@ -353,8 +367,8 @@ bool higher_lower_game_face_loop(movement_event_t event, void *context) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
// Show your initial UI here.
|
||||
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);
|
||||
watch_display_string("GA", STATUS_DISPLAY_START);
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
// If needed, update your display here.
|
||||
@@ -96,8 +96,7 @@ static inline void _inc_uint8(uint8_t *value, uint8_t step, uint8_t max) {
|
||||
|
||||
static uint32_t _get_now_ts() {
|
||||
// returns the current date time as unix timestamp
|
||||
watch_date_time_t now = watch_rtc_get_date_time();
|
||||
return watch_utility_date_time_to_unix_time(now, 0);
|
||||
return movement_get_utc_timestamp();
|
||||
}
|
||||
|
||||
static inline void _button_beep() {
|
||||
|
||||
@@ -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 ( );
|
||||
}
|
||||
@@ -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_
|
||||
@@ -188,6 +188,15 @@ bool moon_phase_face_loop(movement_event_t event, void *context) {
|
||||
state->offset = 0;
|
||||
_update(state, state->offset);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
state->offset -= 86400;
|
||||
_update(state, state->offset);
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
movement_illuminate_led();
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
// QUESTION: Should timeout reset offset to 0?
|
||||
break;
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
* each button press, and both the text and the graphical representation will
|
||||
* display the moon phase for that day. Try pressing the Alarm button 27 times
|
||||
* now, just to visualize what the moon will look like over the next month.
|
||||
* Pressing the Light button will move back in time.
|
||||
*
|
||||
* Holding the Light button will illuminate the display.
|
||||
*/
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
int16_t value = pulsometer->pulses;
|
||||
|
||||
if (value < 0) value = 0;
|
||||
if (value > 9999) value = 9999;
|
||||
|
||||
snprintf(buf, sizeof(buf), "%-4hd", value);
|
||||
|
||||
+28
-33
@@ -23,6 +23,7 @@
|
||||
*/
|
||||
|
||||
#include "simon_face.h"
|
||||
#include "delay.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -47,18 +48,19 @@ static inline uint8_t _simon_get_rand_num(uint8_t num_values) {
|
||||
}
|
||||
|
||||
static void _simon_clear_display(simon_state_t *state) {
|
||||
if (state->playing_state == SIMON_NOT_PLAYING) {
|
||||
watch_display_string(" ", 0);
|
||||
} else {
|
||||
sprintf(_simon_display_buf, " %2d ", state->sequence_length);
|
||||
watch_display_string(_simon_display_buf, 0);
|
||||
watch_clear_display();
|
||||
if (state->playing_state != SIMON_NOT_PLAYING) {
|
||||
sprintf(_simon_display_buf, "%2d", state->sequence_length);
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf);
|
||||
}
|
||||
}
|
||||
|
||||
static void _simon_not_playing_display(simon_state_t *state) {
|
||||
_simon_clear_display(state);
|
||||
|
||||
sprintf(_simon_display_buf, "SI %d", state->best_score);
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP, "SIMON", "SI");
|
||||
sprintf(_simon_display_buf, "%d", state->best_score);
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, _simon_display_buf);
|
||||
if (!state->soundOff)
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
else
|
||||
@@ -67,14 +69,13 @@ static void _simon_not_playing_display(simon_state_t *state) {
|
||||
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
else
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
watch_display_string(_simon_display_buf, 0);
|
||||
switch (state->mode)
|
||||
{
|
||||
case SIMON_MODE_EASY:
|
||||
watch_display_string("E", 9);
|
||||
watch_display_text(WATCH_POSITION_SECONDS, " E");
|
||||
break;
|
||||
case SIMON_MODE_HARD:
|
||||
watch_display_string("H", 9);
|
||||
watch_display_text(WATCH_POSITION_SECONDS, " H");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -90,24 +91,27 @@ static void _simon_reset(simon_state_t *state) {
|
||||
|
||||
|
||||
static void _simon_display_note(SimonNote note, simon_state_t *state) {
|
||||
char *ndtemplate = NULL;
|
||||
|
||||
watch_clear_display();
|
||||
if (note == SIMON_WRONG_NOTE) {
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "OH");
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, "NOOOOO");
|
||||
return;
|
||||
}
|
||||
sprintf(_simon_display_buf, "%2d", state->sequence_length);
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf);
|
||||
switch (note) {
|
||||
case SIMON_LED_NOTE:
|
||||
ndtemplate = "LI%2d ";
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "LI");
|
||||
break;
|
||||
case SIMON_ALARM_NOTE:
|
||||
ndtemplate = " %2d AL";
|
||||
watch_display_text(WATCH_POSITION_SECONDS, "AL");
|
||||
break;
|
||||
case SIMON_MODE_NOTE:
|
||||
ndtemplate = " %2dDE ";
|
||||
watch_display_text_with_fallback(WATCH_POSITION_HOURS, "Md", "DE");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case SIMON_WRONG_NOTE:
|
||||
ndtemplate = "OH NOOOOO";
|
||||
}
|
||||
|
||||
sprintf(_simon_display_buf, ndtemplate, state->sequence_length);
|
||||
watch_display_string(_simon_display_buf, 0);
|
||||
}
|
||||
|
||||
static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_rest) {
|
||||
@@ -115,30 +119,22 @@ static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_res
|
||||
switch (note) {
|
||||
case SIMON_LED_NOTE:
|
||||
if (!state->lightOff) watch_set_led_yellow();
|
||||
if (state->soundOff)
|
||||
if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
|
||||
delay_ms(_delay_beep);
|
||||
else
|
||||
watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
|
||||
break;
|
||||
case SIMON_MODE_NOTE:
|
||||
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);
|
||||
else
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep);
|
||||
break;
|
||||
case SIMON_ALARM_NOTE:
|
||||
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);
|
||||
else
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep);
|
||||
break;
|
||||
case SIMON_WRONG_NOTE:
|
||||
if (state->soundOff)
|
||||
if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_A1, 800);
|
||||
delay_ms(800);
|
||||
else
|
||||
watch_buzzer_play_note(BUZZER_NOTE_A1, 800);
|
||||
break;
|
||||
}
|
||||
watch_set_led_off();
|
||||
@@ -146,7 +142,7 @@ static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_res
|
||||
if (note != SIMON_WRONG_NOTE) {
|
||||
_simon_clear_display(state);
|
||||
if (!skip_rest) {
|
||||
watch_buzzer_play_note(BUZZER_NOTE_REST, (_delay_beep * 2)/3);
|
||||
delay_ms((_delay_beep * 2)/3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,7 +216,6 @@ void simon_face_setup(uint8_t watch_face_index,
|
||||
}
|
||||
|
||||
void simon_face_activate(void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
simon_state_t *state = (simon_state_t *)context;
|
||||
_simon_change_speed(state);
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "simple_coin_flip_face.h"
|
||||
#include "delay.h"
|
||||
|
||||
void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) {
|
||||
(void) watch_face_index;
|
||||
@@ -36,7 +37,7 @@ void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr)
|
||||
}
|
||||
|
||||
void simple_coin_flip_face_activate(void *context) {
|
||||
simple_coin_flip_face_state_t *state = (simple_coin_flip_face_state_t *)context;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
static uint32_t get_random(uint32_t max) {
|
||||
@@ -48,7 +49,7 @@ static uint32_t get_random(uint32_t max) {
|
||||
|
||||
}
|
||||
|
||||
void draw_start_face() {
|
||||
static void draw_start_face(void) {
|
||||
watch_clear_display();
|
||||
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, " Flip");
|
||||
@@ -57,7 +58,7 @@ void draw_start_face() {
|
||||
}
|
||||
}
|
||||
|
||||
void set_pixels(int pixels[3][4][2], int j_len) {
|
||||
static void set_pixels(int pixels[3][4][2], int j_len) {
|
||||
for(int loopruns = 0; loopruns<2; loopruns++) {
|
||||
for(int i = 0; i<3; i++) {
|
||||
watch_clear_display();
|
||||
@@ -69,7 +70,7 @@ void set_pixels(int pixels[3][4][2], int j_len) {
|
||||
}
|
||||
}
|
||||
|
||||
void load_animation() {
|
||||
static void load_animation(void) {
|
||||
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
|
||||
int j_len = 2;
|
||||
int pixels[3][4][2] = {
|
||||
@@ -114,6 +115,7 @@ void load_animation() {
|
||||
}
|
||||
|
||||
static void _blink_face_update_lcd(simple_coin_flip_face_state_t *state) {
|
||||
(void) state;
|
||||
watch_clear_display();
|
||||
load_animation();
|
||||
watch_clear_display();
|
||||
|
||||
@@ -82,9 +82,8 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) {
|
||||
}
|
||||
|
||||
watch_date_time_t date_time = movement_get_local_date_time(); // the current local date / time
|
||||
watch_date_time_t utc_now = watch_utility_date_time_convert_zone(date_time, movement_get_current_timezone_offset(), 0); // the current date / time in UTC
|
||||
watch_date_time_t scratch_time; // scratchpad, contains different values at different times
|
||||
scratch_time.reg = utc_now.reg;
|
||||
scratch_time.reg = date_time.reg;
|
||||
|
||||
// Weird quirky unsigned things were happening when I tried to cast these directly to doubles below.
|
||||
// it looks redundant, but extracting them to local int16's seemed to fix it.
|
||||
@@ -200,7 +199,7 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) {
|
||||
}
|
||||
|
||||
// it's after sunset. we need to display sunrise/sunset for tomorrow.
|
||||
uint32_t timestamp = watch_utility_date_time_to_unix_time(utc_now, 0);
|
||||
uint32_t timestamp = watch_utility_date_time_to_unix_time(date_time, 0);
|
||||
timestamp += 86400;
|
||||
scratch_time = watch_utility_date_time_from_unix_time(timestamp, 0);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,12 @@ static bool tally_face_should_move_back(tally_state_t *state) {
|
||||
bool tally_face_loop(movement_event_t event, void *context) {
|
||||
tally_state_t *state = (tally_state_t *)context;
|
||||
static bool using_led = false;
|
||||
static int8_t beep_sequence[] = {
|
||||
0, 2,
|
||||
BUZZER_NOTE_REST, 3,
|
||||
0, 2,
|
||||
0
|
||||
};
|
||||
|
||||
if (using_led) {
|
||||
if(!HAL_GPIO_BTN_MODE_read() && !HAL_GPIO_BTN_LIGHT_read() && !HAL_GPIO_BTN_ALARM_read())
|
||||
@@ -148,9 +154,11 @@ bool tally_face_loop(movement_event_t event, void *context) {
|
||||
state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index
|
||||
_init_val = true;
|
||||
//play a reset tune
|
||||
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30);
|
||||
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30);
|
||||
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
|
||||
if (movement_button_should_sound()) {
|
||||
beep_sequence[0] = BUZZER_NOTE_G6;
|
||||
beep_sequence[4] = BUZZER_NOTE_E6;
|
||||
movement_play_sequence(beep_sequence, 0);
|
||||
}
|
||||
print_tally(state, movement_button_should_sound());
|
||||
}
|
||||
break;
|
||||
@@ -168,9 +176,11 @@ bool tally_face_loop(movement_event_t event, void *context) {
|
||||
if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){
|
||||
state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE();
|
||||
state->tally_idx = _tally_default[state->tally_default_idx];
|
||||
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
|
||||
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30);
|
||||
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30);
|
||||
if (movement_button_should_sound()) {
|
||||
beep_sequence[0] = BUZZER_NOTE_E6;
|
||||
beep_sequence[4] = BUZZER_NOTE_G6;
|
||||
movement_play_sequence(beep_sequence, 0);
|
||||
}
|
||||
print_tally(state, movement_button_should_sound());
|
||||
}
|
||||
else{
|
||||
|
||||
@@ -280,6 +280,9 @@ void tarot_face_setup(uint8_t watch_face_index, void ** context_ptr) {
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(tarot_state_t));
|
||||
memset(*context_ptr, 0, sizeof(tarot_state_t));
|
||||
tarot_state_t *state = (tarot_state_t *)*context_ptr;
|
||||
state->major_arcana_only = true;
|
||||
state->num_cards_to_draw = 3;
|
||||
}
|
||||
// Emulator only: Seed random number generator
|
||||
#if __EMSCRIPTEN__
|
||||
@@ -292,8 +295,6 @@ void tarot_face_activate(void *context) {
|
||||
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP, "Tarot", "TA");
|
||||
init_deck(state);
|
||||
state->num_cards_to_draw = 3;
|
||||
state->major_arcana_only = true;
|
||||
}
|
||||
|
||||
bool tarot_face_loop(movement_event_t event, void *context) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_
|
||||
@@ -36,7 +36,6 @@
|
||||
#include <string.h>
|
||||
#include "totp_face.h"
|
||||
#include "watch.h"
|
||||
#include "watch_utility.h"
|
||||
#include "TOTP.h"
|
||||
#include "base32.h"
|
||||
|
||||
@@ -159,7 +158,7 @@ static void totp_generate_and_display(totp_state_t *totp_state) {
|
||||
}
|
||||
|
||||
static inline uint32_t totp_compute_base_timestamp() {
|
||||
return watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0);
|
||||
return movement_get_utc_timestamp();
|
||||
}
|
||||
|
||||
void totp_face_setup(uint8_t watch_face_index, void ** context_ptr) {
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
#include "base32.h"
|
||||
|
||||
#include "watch.h"
|
||||
#include "watch_utility.h"
|
||||
#include "filesystem.h"
|
||||
|
||||
#include "totp_lfs_face.h"
|
||||
@@ -253,7 +252,7 @@ void totp_lfs_face_activate(void *context) {
|
||||
}
|
||||
#endif
|
||||
|
||||
totp_state->timestamp = watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0);
|
||||
totp_state->timestamp = movement_get_utc_timestamp();
|
||||
totp_face_set_record(totp_state, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
/*
|
||||
|
||||
The displayed Japanese Era can be changed by the buttons on the watch, making it also usable as a converter between the Gregorian calendar and the Japanese Era.
|
||||
|
||||
Light button: Subtract one year from the Japanese Era.
|
||||
Start/Stop button: Add one year to the Japanese Era.
|
||||
Button operations support long-press functionality.
|
||||
|
||||
Japanese Era Notations:
|
||||
|
||||
r : REIWA (令和)
|
||||
h : HEISEI (平成)
|
||||
s : SHOWA(昭和)
|
||||
*/
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2025 kbc-yam
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
* persons to whom the Software is furnished to do so, subject to the
|
||||
* following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
|
||||
* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
|
||||
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -1,6 +1,47 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2025 kbc-yam
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
* persons to whom the Software is furnished to do so, subject to the
|
||||
* following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
|
||||
* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
|
||||
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef WAREKI_FACE_H_
|
||||
#define WAREKI_FACE_H_
|
||||
|
||||
/*
|
||||
Display Japanese era names (Wareki)
|
||||
|
||||
The displayed Japanese Era can be changed by the buttons on the watch, making it also usable as a converter between the Gregorian calendar and the Japanese Era.
|
||||
|
||||
Light button: Subtract one year from the Japanese Era.
|
||||
Start/Stop button: Add one year to the Japanese Era.
|
||||
Button operations support long-press functionality.
|
||||
|
||||
Japanese Era Notations:
|
||||
|
||||
r : REIWA (令和)
|
||||
h : HEISEI (平成)
|
||||
s : SHOWA(昭和)
|
||||
*/
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
#define REIWA_LIMIT 2018 + 99
|
||||
|
||||
@@ -256,7 +256,7 @@ static void reset_board(wordle_state_t *state) {
|
||||
|
||||
static void display_title(wordle_state_t *state) {
|
||||
state->curr_screen = WORDLE_SCREEN_TITLE;
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "WO");
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO");
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, "WordLE");
|
||||
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
|
||||
@@ -286,7 +286,7 @@ static void display_streak(wordle_state_t *state) {
|
||||
#else
|
||||
sprintf(buf, "St%4d", state->streak);
|
||||
#endif
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "WO");
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO");
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, buf);
|
||||
watch_set_colon();
|
||||
@@ -304,7 +304,7 @@ static void display_wait(wordle_state_t *state) {
|
||||
else { // Streak too long to display in top-right
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
}
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "WO");
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO");
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, " WaIt ");
|
||||
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
|
||||
}
|
||||
@@ -321,7 +321,8 @@ static uint32_t get_day_unix_time(void) {
|
||||
static void display_lose(wordle_state_t *state, uint8_t subsecond) {
|
||||
char buf[10];
|
||||
sprintf(buf," %s", subsecond % 2 ? _valid_words[state->curr_answer] : " ");
|
||||
watch_display_text(WATCH_POSITION_TOP, "L ");
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP, "LOSE", "L ");
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, buf);
|
||||
}
|
||||
|
||||
@@ -329,7 +330,8 @@ static void display_win(wordle_state_t *state, uint8_t subsecond) {
|
||||
(void) state;
|
||||
char buf[10];
|
||||
sprintf(buf," %s ", subsecond % 2 ? "NICE" : "JOb ");
|
||||
watch_display_text(WATCH_POSITION_TOP, "W ");
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "W ");
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, buf);
|
||||
}
|
||||
|
||||
@@ -644,4 +646,3 @@ bool wordle_face_loop(movement_event_t event, void *context) {
|
||||
void wordle_face_resign(void *context) {
|
||||
(void) context;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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, \
|
||||
})
|
||||
@@ -47,9 +47,6 @@ typedef struct {
|
||||
// Selected program
|
||||
chirpy_demo_program_t program;
|
||||
|
||||
// Helps us handle 1/64 ticks during transmission; including countdown timer
|
||||
chirpy_tick_state_t tick_state;
|
||||
|
||||
// Used by chirpy encoder during transmission
|
||||
chirpy_encoder_state_t encoder_state;
|
||||
|
||||
@@ -150,46 +147,10 @@ static void _cdf_update_lcd(chirpy_demo_state_t *state) {
|
||||
}
|
||||
}
|
||||
|
||||
static void _cdf_quit_chirping(chirpy_demo_state_t *state) {
|
||||
state->mode = CDM_CHOOSE;
|
||||
watch_set_buzzer_off();
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
movement_request_tick_frequency(1);
|
||||
}
|
||||
|
||||
static void _cdf_scale_tick(void *context) {
|
||||
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
|
||||
chirpy_tick_state_t *tick_state = &state->tick_state;
|
||||
|
||||
// Scale goes in 200Hz increments from 700 Hz to 12.3 kHz -> 58 steps
|
||||
if (tick_state->seq_pos == 58) {
|
||||
_cdf_quit_chirping(state);
|
||||
return;
|
||||
}
|
||||
uint32_t freq = 700 + tick_state->seq_pos * 200;
|
||||
uint32_t period = 1000000 / freq;
|
||||
watch_set_buzzer_period_and_duty_cycle(period, 25);
|
||||
watch_set_buzzer_on();
|
||||
++tick_state->seq_pos;
|
||||
}
|
||||
|
||||
static void _cdf_data_tick(void *context) {
|
||||
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
|
||||
|
||||
uint8_t tone = chirpy_get_next_tone(&state->encoder_state);
|
||||
// Transmission over?
|
||||
if (tone == 255) {
|
||||
_cdf_quit_chirping(state);
|
||||
return;
|
||||
}
|
||||
uint16_t period = chirpy_get_tone_period(tone);
|
||||
watch_set_buzzer_period_and_duty_cycle(period, 25);
|
||||
watch_set_buzzer_on();
|
||||
}
|
||||
|
||||
static uint8_t *curr_data_ptr;
|
||||
static uint16_t curr_data_ix;
|
||||
static uint16_t curr_data_len;
|
||||
static chirpy_demo_state_t *curr_state;
|
||||
|
||||
static uint8_t _cdf_get_next_byte(uint8_t *next_byte) {
|
||||
if (curr_data_ix == curr_data_len)
|
||||
@@ -199,25 +160,46 @@ static uint8_t _cdf_get_next_byte(uint8_t *next_byte) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void _cdf_countdown_tick(void *context) {
|
||||
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
|
||||
chirpy_tick_state_t *tick_state = &state->tick_state;
|
||||
|
||||
// Countdown over: start actual broadcast
|
||||
if (tick_state->seq_pos == 8 * 3) {
|
||||
tick_state->tick_compare = 3;
|
||||
tick_state->tick_count = -1;
|
||||
tick_state->seq_pos = 0;
|
||||
// We'll be chirping out a scale
|
||||
if (false) { // state->program == CDP_CLEAR) {
|
||||
tick_state->tick_fun = _cdf_scale_tick;
|
||||
static void _cdf_on_chirping_done(void) {
|
||||
if (curr_state) {
|
||||
curr_state->mode = CDM_CHOOSE;
|
||||
}
|
||||
// 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;
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
}
|
||||
|
||||
static bool _cdm_raw_source_fn(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration) {
|
||||
// Beep countdown
|
||||
if (position < 6) {
|
||||
if (position % 2) {
|
||||
*period = WATCH_BUZZER_PERIOD_REST;
|
||||
*duration = 56;
|
||||
} else {
|
||||
*period = NotePeriods[BUZZER_NOTE_A5];
|
||||
*duration = 8;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
chirpy_demo_state_t *state = (chirpy_demo_state_t *)userdata;
|
||||
|
||||
uint8_t tone = chirpy_get_next_tone(&state->encoder_state);
|
||||
// Transmission over?
|
||||
if (tone == 255) {
|
||||
return true;
|
||||
}
|
||||
|
||||
*period = chirpy_get_tone_period(tone);
|
||||
*duration = 3;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void _cdm_start_transmission(chirpy_demo_state_t *state) {
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
state->mode = CDM_CHIRPING;
|
||||
|
||||
// Set up the data
|
||||
curr_state = state;
|
||||
curr_data_ix = 0;
|
||||
if (state->program == CDP_INFO_SHORT) {
|
||||
curr_data_ptr = short_data;
|
||||
@@ -229,29 +211,9 @@ static void _cdf_countdown_tick(void *context) {
|
||||
curr_data_ptr = activity_buffer;
|
||||
curr_data_len = activity_buffer_size;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Sound or turn off buzzer
|
||||
if ((tick_state->seq_pos % 8) == 0) {
|
||||
watch_set_buzzer_period_and_duty_cycle(NotePeriods[BUZZER_NOTE_A5], 25);
|
||||
watch_set_buzzer_on();
|
||||
} else if ((tick_state->seq_pos % 8) == 1) {
|
||||
watch_set_buzzer_off();
|
||||
}
|
||||
++tick_state->seq_pos;
|
||||
}
|
||||
|
||||
static void _cdm_setup_chirp(chirpy_demo_state_t *state) {
|
||||
// We want frequent callbacks from now on
|
||||
movement_request_tick_frequency(64);
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
state->mode = CDM_CHIRPING;
|
||||
// Set up tick state; start with countdown
|
||||
state->tick_state.tick_count = -1;
|
||||
state->tick_state.tick_compare = 8;
|
||||
state->tick_state.seq_pos = 0;
|
||||
state->tick_state.tick_fun = _cdf_countdown_tick;
|
||||
chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte);
|
||||
watch_buzzer_play_raw_source(_cdm_raw_source_fn, state, _cdf_on_chirping_done);
|
||||
}
|
||||
|
||||
bool chirpy_demo_face_loop(movement_event_t event, void *context) {
|
||||
@@ -261,12 +223,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
|
||||
case EVENT_ACTIVATE:
|
||||
_cdf_update_lcd(state);
|
||||
break;
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
// Do not exit face while we're chirping
|
||||
if (state->mode != CDM_CHIRPING) {
|
||||
movement_move_to_next_face();
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
// We don't do light.
|
||||
break;
|
||||
@@ -286,10 +243,6 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
|
||||
state->program = CDP_CLEAR;
|
||||
_cdf_update_lcd(state);
|
||||
}
|
||||
// If chirping: stoppit
|
||||
else if (state->mode == CDM_CHIRPING) {
|
||||
_cdf_quit_chirping(state);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
// If in choose mode: start chirping
|
||||
@@ -299,16 +252,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
|
||||
movement_force_led_off();
|
||||
movement_move_to_next_face();
|
||||
} else {
|
||||
_cdm_setup_chirp(state);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (state->mode == CDM_CHIRPING) {
|
||||
++state->tick_state.tick_count;
|
||||
if (state->tick_state.tick_count == state->tick_state.tick_compare) {
|
||||
state->tick_state.tick_count = 0;
|
||||
state->tick_state.tick_fun(context);
|
||||
_cdm_start_transmission(state);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -317,14 +261,12 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
|
||||
if (state->mode != CDM_CHIRPING) {
|
||||
movement_move_to_face(0);
|
||||
}
|
||||
// fall through
|
||||
default:
|
||||
movement_default_loop_handler(event);
|
||||
break;
|
||||
}
|
||||
|
||||
// Return true if the watch can enter standby mode. False needed when chirping.
|
||||
if (state->mode == CDM_CHIRPING)
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,13 @@ void activity_logging_face_activate(void *context) {
|
||||
bool activity_logging_face_loop(movement_event_t event, void *context) {
|
||||
activity_logging_state_t *state = (activity_logging_state_t *)context;
|
||||
switch (event.event_type) {
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
movement_illuminate_led();
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
state->display_index = (state->display_index + ACTIVITY_LOGGING_NUM_DAYS - 1) % ACTIVITY_LOGGING_NUM_DAYS;
|
||||
_activity_logging_face_update_display(state);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->display_index = (state->display_index + 1) % ACTIVITY_LOGGING_NUM_DAYS;
|
||||
// fall through
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
*
|
||||
* A short press of the Alarm button moves backwards in the data log, showing yesterday's active minutes,
|
||||
* then the day before, etc. going back 14 days.
|
||||
* A short press of the Light button moves forward in the data log, looping around if we're on the most-recent day.
|
||||
* Holding the Light button will illuminate the display.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
@@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state)
|
||||
lis2dw_fifo_t fifo;
|
||||
float x = 0, y = 0, z = 0;
|
||||
|
||||
lis2dw_read_fifo(&fifo);
|
||||
lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT / DISPLAY_FREQUENCY);
|
||||
if (fifo.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include <math.h>
|
||||
#include "finetune_face.h"
|
||||
#include "nanosec_face.h"
|
||||
#include "watch_utility.h"
|
||||
#include "delay.h"
|
||||
|
||||
extern nanosec_state_t nanosec_state;
|
||||
@@ -51,7 +50,7 @@ void finetune_face_activate(void *context) {
|
||||
}
|
||||
|
||||
static float finetune_get_hours_passed(void) {
|
||||
uint32_t current_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0);
|
||||
uint32_t current_time = movement_get_utc_timestamp();
|
||||
return (current_time - nanosec_state.last_correction_time) / 3600.0f;
|
||||
}
|
||||
|
||||
@@ -64,7 +63,7 @@ static void finetune_update_display(void) {
|
||||
|
||||
if (finetune_page == 0) {
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "FTU", "FT");
|
||||
watch_date_time_t date_time = watch_rtc_get_date_time();
|
||||
watch_date_time_t date_time = movement_get_utc_date_time();
|
||||
sprintf(buf, "%04d%02d", abs(total_adjustment), date_time.unit.second);
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, buf);
|
||||
|
||||
@@ -106,17 +105,9 @@ static void finetune_adjust_subseconds(int delta) {
|
||||
watch_rtc_enable(false);
|
||||
delay_ms(delta);
|
||||
if (delta > 500) {
|
||||
watch_date_time_t date_time = watch_rtc_get_date_time();
|
||||
date_time.unit.second = (date_time.unit.second + 1) % 60;
|
||||
if (date_time.unit.second == 0) { // Overflow
|
||||
date_time.unit.minute = (date_time.unit.minute + 1) % 60;
|
||||
if (date_time.unit.minute == 0) { // Overflow
|
||||
date_time.unit.hour = (date_time.unit.hour + 1) % 24;
|
||||
if (date_time.unit.hour == 0) // Overflow
|
||||
date_time.unit.day++;
|
||||
}
|
||||
}
|
||||
watch_rtc_set_date_time(date_time);
|
||||
uint32_t timestamp = movement_get_utc_timestamp();
|
||||
timestamp += 1;
|
||||
movement_set_utc_timestamp(timestamp);
|
||||
}
|
||||
watch_rtc_enable(true);
|
||||
}
|
||||
@@ -126,7 +117,7 @@ static void finetune_update_correction_time(void) {
|
||||
nanosec_state.freq_correction += roundf(nanosec_get_aging() * 100);
|
||||
|
||||
// Remember when we last corrected time
|
||||
nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0);
|
||||
nanosec_state.last_correction_time = movement_get_utc_timestamp();
|
||||
nanosec_save();
|
||||
movement_move_to_face(0); // Go to main face after saving settings
|
||||
}
|
||||
@@ -146,7 +137,7 @@ bool finetune_face_loop(movement_event_t event, void *context) {
|
||||
// We flash green LED once per minute to measure clock error, when we are not on first screen
|
||||
if (finetune_page!=0) {
|
||||
watch_date_time_t date_time;
|
||||
date_time = watch_rtc_get_date_time();
|
||||
date_time = movement_get_utc_date_time();
|
||||
if (date_time.unit.second == 0) {
|
||||
watch_set_led_green();
|
||||
#ifndef __EMSCRIPTEN__
|
||||
|
||||
@@ -44,11 +44,6 @@
|
||||
* worry about aging only on second/third years of watch calibration (if you
|
||||
* are really looking at less than 10 seconds per year of error).
|
||||
*
|
||||
* Warning, do not use at the first second of a month, as you might stay at
|
||||
* the same month and it will surprise you. Just wait 1 second...We are not
|
||||
* fully replicating RTC timer behavior when RTC is off.
|
||||
* Simulating months and years is... too much complexity.
|
||||
*
|
||||
* For full usage instructions, please refer to the wiki:
|
||||
* https://www.sensorwatch.net/docs/watchfaces/nanosec/
|
||||
*/
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include <math.h>
|
||||
#include "nanosec_face.h"
|
||||
#include "filesystem.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
int16_t freq_correction_residual = 0; // Dithering 0.1ppm correction, does not need to be configured.
|
||||
int16_t freq_correction_previous = -30000;
|
||||
@@ -44,8 +43,7 @@ const float voltage_coefficient = 0.241666667 * dithering; // 10 * ppm/V. Nomina
|
||||
static void nanosec_init_profile(void) {
|
||||
nanosec_changed = true;
|
||||
nanosec_state.correction_cadence = 10;
|
||||
watch_date_time_t date_time = watch_rtc_get_date_time();
|
||||
nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(date_time, 0);
|
||||
nanosec_state.last_correction_time = movement_get_utc_timestamp();
|
||||
|
||||
// init data after changing profile - do that once per profile selection
|
||||
switch (nanosec_state.correction_profile) {
|
||||
@@ -265,8 +263,8 @@ static void nanosec_next_edit_screen(void) {
|
||||
|
||||
float nanosec_get_aging() // Returns aging correction in ppm
|
||||
{
|
||||
watch_date_time_t date_time = watch_rtc_get_date_time();
|
||||
float years = (watch_utility_date_time_to_unix_time(date_time, 0) - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune
|
||||
uint32_t timestamp = movement_get_utc_timestamp();
|
||||
float years = (timestamp - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune
|
||||
return years*nanosec_state.aging_ppm_pa/100.0f;
|
||||
}
|
||||
|
||||
@@ -377,7 +375,7 @@ movement_watch_face_advisory_t nanosec_face_advise(void *context) {
|
||||
|
||||
// No need for background correction if we are on profile 0 - static hardware correction.
|
||||
if (nanosec_state.correction_profile != 0) {
|
||||
watch_date_time_t date_time = watch_rtc_get_date_time();
|
||||
watch_date_time_t date_time = movement_get_utc_date_time();
|
||||
retval.wants_background_task = date_time.unit.minute % nanosec_state.correction_cadence == 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ static void _handle_alarm_button(watch_date_time_t date_time, uint8_t current_pa
|
||||
current_offset = movement_get_current_timezone_offset_for_zone(movement_get_timezone_index());
|
||||
return;
|
||||
case 0: // year
|
||||
date_time.unit.year = ((date_time.unit.year % 60) + 1);
|
||||
date_time.unit.year = (date_time.unit.year + 1) % 60;
|
||||
break;
|
||||
case 1: // month
|
||||
date_time.unit.month = (date_time.unit.month % 12) + 1;
|
||||
@@ -91,6 +91,8 @@ bool set_time_face_loop(movement_event_t event, void *context) {
|
||||
watch_date_time_t date_time = movement_get_local_date_time();
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (_quick_ticks_running) {
|
||||
if (HAL_GPIO_BTN_ALARM_read()) _handle_alarm_button(date_time, current_page);
|
||||
@@ -106,10 +108,6 @@ bool set_time_face_loop(movement_event_t event, void *context) {
|
||||
case EVENT_ALARM_LONG_UP:
|
||||
_abort_quick_ticks();
|
||||
break;
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
_abort_quick_ticks();
|
||||
movement_move_to_next_face();
|
||||
return false;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS;
|
||||
*((uint8_t *)context) = current_page;
|
||||
@@ -185,6 +183,6 @@ bool set_time_face_loop(movement_event_t event, void *context) {
|
||||
|
||||
void set_time_face_resign(void *context) {
|
||||
(void) context;
|
||||
watch_set_led_off();
|
||||
movement_store_settings();
|
||||
movement_request_tick_frequency(1);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,64 @@ static void beep_setting_advance(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static void signal_setting_display(uint8_t subsecond) {
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SIG", "SI");
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, "SIGNAL");
|
||||
if (subsecond % 2) {
|
||||
if (movement_signal_volume() == WATCH_BUZZER_VOLUME_LOUD) {
|
||||
// H for HIGH
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " H");
|
||||
}
|
||||
else {
|
||||
// L for LOW
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " L");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void signal_setting_advance(void) {
|
||||
if (movement_signal_volume() == WATCH_BUZZER_VOLUME_SOFT) {
|
||||
// was soft. make it loud.
|
||||
movement_set_signal_volume(WATCH_BUZZER_VOLUME_LOUD);
|
||||
} else {
|
||||
// was loud. make it soft.
|
||||
movement_set_signal_volume(WATCH_BUZZER_VOLUME_SOFT);
|
||||
}
|
||||
|
||||
signal_setting_display(1);
|
||||
movement_play_signal();
|
||||
}
|
||||
|
||||
|
||||
static void alarm_setting_display(uint8_t subsecond) {
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "ALM", "AL");
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, "ALARM ");
|
||||
if (subsecond % 2) {
|
||||
if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_LOUD) {
|
||||
// H for HIGH
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " H");
|
||||
}
|
||||
else {
|
||||
// L for LOW
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " L");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void alarm_setting_advance(void) {
|
||||
if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_SOFT) {
|
||||
// was soft. make it loud.
|
||||
movement_set_alarm_volume(WATCH_BUZZER_VOLUME_LOUD);
|
||||
} else {
|
||||
// was loud. make it soft.
|
||||
movement_set_alarm_volume(WATCH_BUZZER_VOLUME_SOFT);
|
||||
|
||||
}
|
||||
|
||||
alarm_setting_display(1);
|
||||
movement_play_alarm();
|
||||
}
|
||||
|
||||
static void timeout_setting_display(uint8_t subsecond) {
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP, "TMOUt", "TO");
|
||||
if (subsecond % 2) {
|
||||
@@ -235,7 +293,7 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) {
|
||||
settings_state_t *state = (settings_state_t *)*context_ptr;
|
||||
int8_t current_setting = 0;
|
||||
|
||||
state->num_settings = 5; // baseline, without LED settings
|
||||
state->num_settings = 7; // baseline, without LED settings
|
||||
#ifdef BUILD_GIT_HASH
|
||||
state->num_settings++;
|
||||
#endif
|
||||
@@ -256,6 +314,12 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) {
|
||||
state->settings_screens[current_setting].display = beep_setting_display;
|
||||
state->settings_screens[current_setting].advance = beep_setting_advance;
|
||||
current_setting++;
|
||||
state->settings_screens[current_setting].display = signal_setting_display;
|
||||
state->settings_screens[current_setting].advance = signal_setting_advance;
|
||||
current_setting++;
|
||||
state->settings_screens[current_setting].display = alarm_setting_display;
|
||||
state->settings_screens[current_setting].advance = alarm_setting_advance;
|
||||
current_setting++;
|
||||
state->settings_screens[current_setting].display = timeout_setting_display;
|
||||
state->settings_screens[current_setting].advance = timeout_setting_advance;
|
||||
current_setting++;
|
||||
@@ -322,7 +386,7 @@ bool settings_face_loop(movement_event_t event, void *context) {
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
movement_force_led_off();
|
||||
movement_move_to_next_face();
|
||||
return false;
|
||||
return true;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
state->settings_screens[state->current_page].advance();
|
||||
break;
|
||||
@@ -339,7 +403,7 @@ bool settings_face_loop(movement_event_t event, void *context) {
|
||||
movement_force_led_on(color.red | color.red << 4,
|
||||
color.green | color.green << 4,
|
||||
color.blue | color.blue << 4);
|
||||
return false;
|
||||
return true;
|
||||
} else {
|
||||
movement_force_led_off();
|
||||
return true;
|
||||
|
||||
@@ -45,6 +45,12 @@
|
||||
* a beep when pressed, and if so, how loud it should be. Options are
|
||||
* "Y" for yes and "N" for no.
|
||||
*
|
||||
* SI / SIG - Signal beep.
|
||||
* This setting allows you to choose the hourly chime buzzer volume.
|
||||
*
|
||||
* AL / ALM - Alarm beep.
|
||||
* This setting allows you to choose the alarm buzzer volume.
|
||||
*
|
||||
* TO / Tmout - Timeout.
|
||||
* Sets the time until screens that time out (like Settings and Time Set)
|
||||
* snap back to the first screen. 60 seconds is a good default for the
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ void sleep(const uint8_t mode) {
|
||||
}
|
||||
|
||||
void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool level) {
|
||||
uint32_t config = RTC->MODE2.TAMPCTRL.reg;
|
||||
uint32_t config = RTC->MODE0.TAMPCTRL.reg;
|
||||
|
||||
if (pin == HAL_GPIO_BTN_ALARM_pin()) {
|
||||
HAL_GPIO_BTN_ALARM_in();
|
||||
@@ -71,22 +71,22 @@ void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool leve
|
||||
}
|
||||
|
||||
// disable the RTC
|
||||
RTC->MODE2.CTRLA.bit.ENABLE = 0;
|
||||
while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled
|
||||
RTC->MODE0.CTRLA.bit.ENABLE = 0;
|
||||
while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled
|
||||
|
||||
// update the configuration
|
||||
RTC->MODE2.TAMPCTRL.reg = config;
|
||||
RTC->MODE0.TAMPCTRL.reg = config;
|
||||
|
||||
// re-enable the RTC
|
||||
RTC->MODE2.CTRLA.bit.ENABLE = 1;
|
||||
RTC->MODE0.CTRLA.bit.ENABLE = 1;
|
||||
|
||||
NVIC_ClearPendingIRQ(RTC_IRQn);
|
||||
NVIC_EnableIRQ(RTC_IRQn);
|
||||
RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_TAMPER;
|
||||
RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_TAMPER;
|
||||
}
|
||||
|
||||
void watch_disable_extwake_interrupt(uint8_t pin) {
|
||||
uint32_t config = RTC->MODE2.TAMPCTRL.reg;
|
||||
uint32_t config = RTC->MODE0.TAMPCTRL.reg;
|
||||
|
||||
if (pin == HAL_GPIO_BTN_ALARM_pin()) {
|
||||
btn_alarm_callback = NULL;
|
||||
@@ -101,14 +101,14 @@ void watch_disable_extwake_interrupt(uint8_t pin) {
|
||||
}
|
||||
|
||||
// disable the RTC
|
||||
RTC->MODE2.CTRLA.bit.ENABLE = 0;
|
||||
while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled
|
||||
RTC->MODE0.CTRLA.bit.ENABLE = 0;
|
||||
while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled
|
||||
|
||||
// update the configuration
|
||||
RTC->MODE2.TAMPCTRL.reg = config;
|
||||
RTC->MODE0.TAMPCTRL.reg = config;
|
||||
|
||||
// re-enable the RTC
|
||||
RTC->MODE2.CTRLA.bit.ENABLE = 1;
|
||||
RTC->MODE0.CTRLA.bit.ENABLE = 1;
|
||||
}
|
||||
|
||||
void watch_store_backup_data(uint32_t data, uint8_t reg) {
|
||||
@@ -151,7 +151,8 @@ static void _watch_disable_all_pins_except_rtc(void) {
|
||||
}
|
||||
|
||||
static void _watch_disable_all_peripherals_except_slcd(void) {
|
||||
_watch_disable_tcc();
|
||||
watch_disable_leds();
|
||||
watch_disable_buzzer();
|
||||
watch_disable_adc();
|
||||
watch_disable_external_interrupts();
|
||||
|
||||
|
||||
@@ -38,27 +38,31 @@ void watch_disable_i2c(void) {
|
||||
i2c_disable();
|
||||
}
|
||||
|
||||
void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) {
|
||||
i2c_write(addr, buf, length);
|
||||
int8_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) {
|
||||
return (int8_t)i2c_write(addr, buf, length);
|
||||
}
|
||||
|
||||
void watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) {
|
||||
i2c_read(addr, buf, length);
|
||||
int8_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) {
|
||||
return (int8_t)i2c_read(addr, buf, length);
|
||||
}
|
||||
|
||||
void watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) {
|
||||
int8_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) {
|
||||
uint8_t buf[2];
|
||||
buf[0] = reg;
|
||||
buf[1] = data;
|
||||
|
||||
watch_i2c_send(addr, (uint8_t *)&buf, 2);
|
||||
return (int8_t)watch_i2c_send(addr, (uint8_t *)&buf, 2);
|
||||
}
|
||||
|
||||
uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) {
|
||||
uint8_t data;
|
||||
|
||||
watch_i2c_send(addr, (uint8_t *)®, 1);
|
||||
watch_i2c_receive(addr, (uint8_t *)&data, 1);
|
||||
if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) {
|
||||
return 0;
|
||||
}
|
||||
if (watch_i2c_receive(addr, (uint8_t *)&data, 1) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -66,9 +70,12 @@ uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) {
|
||||
uint16_t watch_i2c_read16(int16_t addr, uint8_t reg) {
|
||||
uint16_t data;
|
||||
|
||||
watch_i2c_send(addr, (uint8_t *)®, 1);
|
||||
watch_i2c_receive(addr, (uint8_t *)&data, 2);
|
||||
|
||||
if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) {
|
||||
return 0;
|
||||
}
|
||||
if (watch_i2c_receive(addr, (uint8_t *)&data, 2) != 0) {
|
||||
return 0;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -76,18 +83,24 @@ uint32_t watch_i2c_read24(int16_t addr, uint8_t reg) {
|
||||
uint32_t data;
|
||||
data = 0;
|
||||
|
||||
watch_i2c_send(addr, (uint8_t *)®, 1);
|
||||
watch_i2c_receive(addr, (uint8_t *)&data, 3);
|
||||
|
||||
if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) {
|
||||
return 0;
|
||||
}
|
||||
if (watch_i2c_receive(addr, (uint8_t *)&data, 3) != 0) {
|
||||
return 0;
|
||||
}
|
||||
return data << 8;
|
||||
}
|
||||
|
||||
uint32_t watch_i2c_read32(int16_t addr, uint8_t reg) {
|
||||
uint32_t data;
|
||||
|
||||
watch_i2c_send(addr, (uint8_t *)®, 1);
|
||||
watch_i2c_receive(addr, (uint8_t *)&data, 4);
|
||||
|
||||
if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) {
|
||||
return 0;
|
||||
}
|
||||
if (watch_i2c_receive(addr, (uint8_t *)&data, 4) != 0) {
|
||||
return 0;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020 Joey Castillo
|
||||
* Copyright (c) 2025 Alessandro Genova
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -23,11 +24,36 @@
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include "watch_rtc.h"
|
||||
#include "watch_private.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
static const uint32_t RTC_OSC_DIV = 10;
|
||||
static const uint32_t RTC_OSC_HZ = 1 << RTC_OSC_DIV; // 2^10 = 1024
|
||||
static const uint32_t RTC_PRESCALER_DIV = 3;
|
||||
static const uint32_t RTC_CNT_HZ = RTC_OSC_HZ >> RTC_PRESCALER_DIV; // 1024 / 2^3 = 128
|
||||
static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1;
|
||||
static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7
|
||||
static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60;
|
||||
|
||||
static const uint32_t RTC_COMP_GRACE_PERIOD = 4;
|
||||
|
||||
static const int TB_BKUP_REG = 7;
|
||||
|
||||
#define WATCH_RTC_N_COMP_CB 8
|
||||
|
||||
typedef struct {
|
||||
volatile uint32_t counter;
|
||||
volatile watch_cb_t callback;
|
||||
volatile bool enabled;
|
||||
} comp_cb_t;
|
||||
|
||||
volatile uint32_t scheduled_comp_counter;
|
||||
|
||||
watch_cb_t tick_callbacks[8];
|
||||
comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB];
|
||||
watch_cb_t alarm_callback;
|
||||
watch_cb_t btn_alarm_callback;
|
||||
watch_cb_t a2_callback;
|
||||
@@ -46,14 +72,77 @@ void _watch_rtc_init(void) {
|
||||
#endif
|
||||
rtc_enable();
|
||||
rtc_configure_callback(watch_rtc_callback);
|
||||
|
||||
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
|
||||
comp_callbacks[index].counter = 0;
|
||||
comp_callbacks[index].callback = NULL;
|
||||
comp_callbacks[index].enabled = false;
|
||||
}
|
||||
|
||||
scheduled_comp_counter = 0;
|
||||
|
||||
NVIC_ClearPendingIRQ(RTC_IRQn);
|
||||
NVIC_EnableIRQ(RTC_IRQn);
|
||||
}
|
||||
|
||||
void watch_rtc_set_date_time(rtc_date_time_t date_time) {
|
||||
rtc_set_date_time(date_time);
|
||||
watch_rtc_set_unix_time(watch_utility_date_time_to_unix_time(date_time, 0));
|
||||
}
|
||||
|
||||
rtc_date_time_t watch_rtc_get_date_time(void) {
|
||||
return rtc_get_date_time();
|
||||
static struct {
|
||||
unix_timestamp_t timestamp;
|
||||
rtc_date_time_t datetime;
|
||||
} cached_date_time = {.datetime.reg=0, .timestamp=0};
|
||||
|
||||
unix_timestamp_t timestamp = watch_rtc_get_unix_time();
|
||||
|
||||
if (timestamp != cached_date_time.timestamp) {
|
||||
cached_date_time.timestamp = timestamp;
|
||||
cached_date_time.datetime = watch_utility_date_time_from_unix_time(timestamp, 0);
|
||||
}
|
||||
|
||||
return cached_date_time.datetime;
|
||||
}
|
||||
|
||||
void watch_rtc_set_unix_time(unix_timestamp_t unix_time) {
|
||||
/* unix_time = time_backup + counter / RTC_CNT_HZ - 0.5
|
||||
*
|
||||
* Because of the way the hardware is designed, the periodic interrupts fire at the subsecond tick values
|
||||
* according to the table below (for a 128Hz counter).
|
||||
* since the 1Hz periodic interrupt is the most important, we shift the conversion from counter to timestamp by 64 ticks,
|
||||
* so that the second changes at the top of the 1Hz interrupt. Hence the 0.5 factor in the equation above.
|
||||
* 1Hz: 64
|
||||
* 2Hz: 32, 96
|
||||
* 4Hz: 16, 48, 80, 112
|
||||
* 8Hz: 8, 24, 40, 56, 72, 88, 104, 120
|
||||
* 16Hz: 4, 12, 20, ..., 124
|
||||
* 32Hz: 2, 6, 10, ..., 126
|
||||
* 64Hz: 1, 3, 5, ..., 127
|
||||
* 128Hz: 0, 1, 2, ..., 127
|
||||
*/
|
||||
rtc_counter_t counter = rtc_get_counter();
|
||||
unix_timestamp_t tb = unix_time - (counter >> RTC_CNT_DIV) - ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) + 1;
|
||||
watch_store_backup_data(tb, TB_BKUP_REG);
|
||||
}
|
||||
|
||||
unix_timestamp_t watch_rtc_get_unix_time(void) {
|
||||
// unix_time = time_backup + counter / RTC_CNT_HZ - 0.5
|
||||
rtc_counter_t counter = rtc_get_counter();
|
||||
unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG);
|
||||
return tb + (counter >> RTC_CNT_DIV) + ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) - 1;
|
||||
}
|
||||
|
||||
rtc_counter_t watch_rtc_get_counter(void) {
|
||||
return rtc_get_counter();
|
||||
}
|
||||
|
||||
uint32_t watch_rtc_get_frequency(void) {
|
||||
return RTC_CNT_HZ;
|
||||
}
|
||||
|
||||
uint32_t watch_rtc_get_ticks_per_minute(void) {
|
||||
return RTC_CNT_TICKS_PER_MINUTE;
|
||||
}
|
||||
|
||||
rtc_date_time_t watch_get_init_date_time(void) {
|
||||
@@ -103,57 +192,123 @@ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency
|
||||
// this also maps nicely to an index for our list of tick callbacks.
|
||||
tick_callbacks[per_n] = callback;
|
||||
|
||||
NVIC_ClearPendingIRQ(RTC_IRQn);
|
||||
NVIC_EnableIRQ(RTC_IRQn);
|
||||
RTC->MODE2.INTENSET.reg = 1 << per_n;
|
||||
// NVIC_ClearPendingIRQ(RTC_IRQn);
|
||||
// NVIC_EnableIRQ(RTC_IRQn);
|
||||
RTC->MODE0.INTENSET.reg = 1 << per_n;
|
||||
}
|
||||
|
||||
void watch_rtc_disable_periodic_callback(uint8_t frequency) {
|
||||
if (__builtin_popcount(frequency) != 1) return;
|
||||
uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24);
|
||||
RTC->MODE2.INTENCLR.reg = 1 << per_n;
|
||||
RTC->MODE0.INTENCLR.reg = 1 << per_n;
|
||||
}
|
||||
|
||||
void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) {
|
||||
RTC->MODE2.INTENCLR.reg = mask;
|
||||
RTC->MODE0.INTENCLR.reg = mask;
|
||||
}
|
||||
|
||||
void watch_rtc_disable_all_periodic_callbacks(void) {
|
||||
watch_rtc_disable_matching_periodic_callbacks(0xFF);
|
||||
}
|
||||
|
||||
void watch_rtc_register_alarm_callback(watch_cb_t callback, rtc_date_time_t alarm_time, rtc_alarm_match_t mask) {
|
||||
RTC->MODE2.Mode2Alarm[0].ALARM.reg = alarm_time.reg;
|
||||
RTC->MODE2.Mode2Alarm[0].MASK.reg = mask;
|
||||
RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0;
|
||||
alarm_callback = callback;
|
||||
NVIC_ClearPendingIRQ(RTC_IRQn);
|
||||
NVIC_EnableIRQ(RTC_IRQn);
|
||||
RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0;
|
||||
void watch_rtc_schedule_next_comp(void) {
|
||||
rtc_counter_t curr_counter = watch_rtc_get_counter();
|
||||
|
||||
// We want to ensure we never miss any registered callbacks,
|
||||
// so if a callback counter has just passed but didn't fire, give it a chance to fire.
|
||||
rtc_counter_t lax_curr_counter = curr_counter - RTC_COMP_GRACE_PERIOD;
|
||||
|
||||
bool schedule_any = false;
|
||||
rtc_counter_t comp_counter;
|
||||
rtc_counter_t min_diff = UINT_MAX;
|
||||
|
||||
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
|
||||
if (comp_callbacks[index].enabled) {
|
||||
rtc_counter_t diff = comp_callbacks[index].counter - lax_curr_counter;
|
||||
if (diff <= min_diff) {
|
||||
min_diff = diff;
|
||||
comp_counter = comp_callbacks[index].counter;
|
||||
schedule_any = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schedule_any) {
|
||||
// If we are changing the comp counter at the front of the line, don't schedule a comp interrupt for a counter that is too close to now
|
||||
if (comp_counter != scheduled_comp_counter) {
|
||||
rtc_counter_t earliest_comp_counter = curr_counter + RTC_COMP_GRACE_PERIOD;
|
||||
if ((earliest_comp_counter - lax_curr_counter) > (comp_counter - lax_curr_counter)) {
|
||||
comp_counter = earliest_comp_counter;
|
||||
}
|
||||
scheduled_comp_counter = comp_counter;
|
||||
rtc_enable_compare_interrupt(comp_counter);
|
||||
}
|
||||
} else {
|
||||
scheduled_comp_counter = lax_curr_counter - RTC_COMP_GRACE_PERIOD;
|
||||
rtc_disable_compare_interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
void watch_rtc_disable_alarm_callback(void) {
|
||||
RTC->MODE2.INTENCLR.reg = RTC_MODE2_INTENCLR_ALARM0;
|
||||
void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) {
|
||||
if (index >= WATCH_RTC_N_COMP_CB) {
|
||||
return;
|
||||
}
|
||||
|
||||
comp_callbacks[index].counter = counter;
|
||||
comp_callbacks[index].callback = callback;
|
||||
comp_callbacks[index].enabled = true;
|
||||
|
||||
watch_rtc_schedule_next_comp();
|
||||
}
|
||||
|
||||
void watch_rtc_callback(uint16_t interrupt_status) {
|
||||
uint16_t interrupt_enabled = RTC->MODE2.INTENSET.reg;
|
||||
void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) {
|
||||
if (index >= WATCH_RTC_N_COMP_CB) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_PER_Msk) {
|
||||
comp_callbacks[index].counter = counter;
|
||||
comp_callbacks[index].callback = callback;
|
||||
comp_callbacks[index].enabled = true;
|
||||
}
|
||||
|
||||
void watch_rtc_disable_comp_callback(uint8_t index) {
|
||||
if (index >= WATCH_RTC_N_COMP_CB) {
|
||||
return;
|
||||
}
|
||||
|
||||
comp_callbacks[index].enabled = false;
|
||||
|
||||
watch_rtc_schedule_next_comp();
|
||||
}
|
||||
|
||||
void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) {
|
||||
if (index >= WATCH_RTC_N_COMP_CB) {
|
||||
return;
|
||||
}
|
||||
|
||||
comp_callbacks[index].enabled = false;
|
||||
}
|
||||
|
||||
void watch_rtc_callback(uint16_t interrupt_cause) {
|
||||
// First read all relevant registers, to ensure no changes occurr during the callbacks
|
||||
rtc_counter_t curr_counter = watch_rtc_get_counter();
|
||||
uint16_t interrupt_enabled = (uint16_t)RTC->MODE0.INTENSET.reg;
|
||||
|
||||
if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) {
|
||||
// handle the tick callback first, it's what we do the most.
|
||||
// start from PER7, the 1 Hz tick.
|
||||
for(int8_t i = 7; i >= 0; i--) {
|
||||
if ((interrupt_status & interrupt_enabled) & (1 << i)) {
|
||||
if ((interrupt_cause & interrupt_enabled) & (1 << i)) {
|
||||
if (tick_callbacks[i] != NULL) {
|
||||
tick_callbacks[i]();
|
||||
}
|
||||
RTC->MODE2.INTFLAG.reg = 1 << i;
|
||||
// break; Uncertain if this fix is requried. We were discussing in discord. Might slightly increase power consumption.
|
||||
}
|
||||
}
|
||||
} else if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_TAMPER) {
|
||||
}
|
||||
|
||||
if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_TAMPER) {
|
||||
// handle the extwake interrupts next.
|
||||
uint8_t reason = RTC->MODE2.TAMPID.reg;
|
||||
uint8_t reason = RTC->MODE0.TAMPID.reg;
|
||||
if (reason & RTC_TAMPID_TAMPID2) {
|
||||
if (btn_alarm_callback != NULL) btn_alarm_callback();
|
||||
} else if (reason & RTC_TAMPID_TAMPID1) {
|
||||
@@ -161,25 +316,36 @@ void watch_rtc_callback(uint16_t interrupt_status) {
|
||||
} else if (reason & RTC_TAMPID_TAMPID0) {
|
||||
if (a4_callback != NULL) a4_callback();
|
||||
}
|
||||
RTC->MODE2.TAMPID.reg = reason;
|
||||
RTC->MODE2.INTFLAG.reg = RTC_MODE2_INTFLAG_TAMPER;
|
||||
} else if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_ALARM0) {
|
||||
// finally handle the alarm.
|
||||
if (alarm_callback != NULL) {
|
||||
alarm_callback();
|
||||
RTC->MODE0.TAMPID.reg = reason;
|
||||
}
|
||||
RTC->MODE2.INTFLAG.reg = RTC_MODE2_INTFLAG_ALARM0;
|
||||
|
||||
if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) {
|
||||
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
|
||||
if (comp_callbacks[index].enabled &&
|
||||
(curr_counter - comp_callbacks[index].counter) < (RTC_COMP_GRACE_PERIOD * 4)
|
||||
) {
|
||||
comp_callbacks[index].enabled = false;
|
||||
comp_callbacks[index].callback();
|
||||
}
|
||||
}
|
||||
watch_rtc_schedule_next_comp();
|
||||
}
|
||||
|
||||
if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_OVF) {
|
||||
// Handle the overflow of the counter. All we need to do is reset the reference time.
|
||||
unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG);
|
||||
watch_store_backup_data(tb + (UINT_MAX >> RTC_CNT_DIV), TB_BKUP_REG);
|
||||
}
|
||||
}
|
||||
|
||||
void watch_rtc_enable(bool en) {
|
||||
// Writing it twice - as it's quite dangerous operation.
|
||||
// If write fails - we might hang with RTC off, which means no recovery possible
|
||||
while (RTC->MODE2.SYNCBUSY.reg);
|
||||
RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0;
|
||||
while (RTC->MODE2.SYNCBUSY.reg);
|
||||
RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0;
|
||||
while (RTC->MODE2.SYNCBUSY.reg);
|
||||
while (RTC->MODE0.SYNCBUSY.reg);
|
||||
RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0;
|
||||
while (RTC->MODE0.SYNCBUSY.reg);
|
||||
RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0;
|
||||
while (RTC->MODE0.SYNCBUSY.reg);
|
||||
}
|
||||
|
||||
void watch_rtc_freqcorr_write(int16_t value, int16_t sign) {
|
||||
@@ -188,8 +354,7 @@ void watch_rtc_freqcorr_write(int16_t value, int16_t sign) {
|
||||
data.bit.VALUE = value;
|
||||
data.bit.SIGN = sign;
|
||||
|
||||
RTC->MODE2.FREQCORR.reg = data.reg; // Setting correction in single write operation
|
||||
RTC->MODE0.FREQCORR.reg = data.reg; // Setting correction in single write operation
|
||||
|
||||
// We do not sycnronize. We are not in a hurry
|
||||
}
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ void watch_enable_display(void) {
|
||||
slcd_clear();
|
||||
|
||||
if (_installed_display == WATCH_LCD_TYPE_CUSTOM) {
|
||||
slcd_set_contrast(4);
|
||||
slcd_set_contrast(0);
|
||||
} else {
|
||||
slcd_set_contrast(9);
|
||||
}
|
||||
@@ -260,6 +260,10 @@ void watch_enable_display(void) {
|
||||
slcd_enable();
|
||||
}
|
||||
|
||||
void watch_disable_display(void) {
|
||||
slcd_disable();
|
||||
}
|
||||
|
||||
inline void watch_set_pixel(uint8_t com, uint8_t seg) {
|
||||
slcd_set_segment(com, seg);
|
||||
}
|
||||
|
||||
@@ -27,14 +27,30 @@
|
||||
#include "tcc.h"
|
||||
#include "tc.h"
|
||||
|
||||
void _watch_enable_tcc(void);
|
||||
void cb_watch_buzzer_seq(void);
|
||||
static void _watch_enable_tcc(void);
|
||||
static void _watch_disable_tcc(void);
|
||||
static void _watch_maybe_enable_tcc(void);
|
||||
static void _watch_maybe_disable_tcc(void);
|
||||
static void _watch_enable_led_pins(void);
|
||||
static void _watch_disable_led_pins(void);
|
||||
static void (*_cb_tc0)(void) = NULL;
|
||||
static void cb_watch_buzzer_seq(void);
|
||||
static void cb_watch_buzzer_raw_source(void);
|
||||
|
||||
static uint16_t _seq_position;
|
||||
static int8_t _tone_ticks, _repeat_counter;
|
||||
static bool _callback_running = false;
|
||||
static int8_t *_sequence;
|
||||
static watch_buzzer_raw_source_t _raw_source;
|
||||
static void* _userdata;
|
||||
static uint8_t _volume;
|
||||
static void (*_cb_finished)(void);
|
||||
static watch_cb_t _cb_start_global = NULL;
|
||||
static watch_cb_t _cb_stop_global = NULL;
|
||||
static volatile bool _led_is_active = false;
|
||||
static volatile bool _buzzer_is_active = false;
|
||||
static volatile uint8_t _current_led_color[3] = {0, 0, 0};
|
||||
|
||||
static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue);
|
||||
|
||||
static void _tcc_write_RUNSTDBY(bool value) {
|
||||
// enables or disables RUNSTDBY of the tcc
|
||||
@@ -46,13 +62,11 @@ static void _tcc_write_RUNSTDBY(bool value) {
|
||||
static inline void _tc0_start() {
|
||||
// start the TC0 timer
|
||||
tc_enable(0);
|
||||
_callback_running = true;
|
||||
}
|
||||
|
||||
static inline void _tc0_stop() {
|
||||
// stop the TC0 timer
|
||||
tc_disable(0);
|
||||
_callback_running = false;
|
||||
}
|
||||
|
||||
static void _tc0_initialize() {
|
||||
@@ -68,19 +82,30 @@ static void _tc0_initialize() {
|
||||
}
|
||||
|
||||
void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) {
|
||||
if (_callback_running) _tc0_stop();
|
||||
watch_buzzer_play_sequence_with_volume(note_sequence, callback_on_end, WATCH_BUZZER_VOLUME_LOUD);
|
||||
}
|
||||
|
||||
void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) {
|
||||
// Abort any previous sequence
|
||||
watch_buzzer_abort_sequence();
|
||||
|
||||
if (_cb_start_global) {
|
||||
_cb_start_global();
|
||||
}
|
||||
|
||||
watch_enable_buzzer();
|
||||
watch_set_buzzer_off();
|
||||
_sequence = note_sequence;
|
||||
_cb_finished = callback_on_end;
|
||||
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
|
||||
_seq_position = 0;
|
||||
_tone_ticks = 0;
|
||||
_repeat_counter = -1;
|
||||
// prepare buzzer
|
||||
watch_enable_buzzer();
|
||||
|
||||
_cb_tc0 = cb_watch_buzzer_seq;
|
||||
// setup TC0 timer
|
||||
_tc0_initialize();
|
||||
// TCC should run in standby mode
|
||||
_tcc_write_RUNSTDBY(true);
|
||||
// start the timer (for the 64 hz callback)
|
||||
_tc0_start();
|
||||
}
|
||||
@@ -110,51 +135,156 @@ void cb_watch_buzzer_seq(void) {
|
||||
// read note
|
||||
watch_buzzer_note_t note = _sequence[_seq_position];
|
||||
if (note != BUZZER_NOTE_REST) {
|
||||
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], 25);
|
||||
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], _volume);
|
||||
watch_set_buzzer_on();
|
||||
} else watch_set_buzzer_off();
|
||||
// set duration ticks and move to next tone
|
||||
_tone_ticks = _sequence[_seq_position + 1];
|
||||
_tone_ticks = _sequence[_seq_position + 1] - 1;
|
||||
_seq_position += 2;
|
||||
} else {
|
||||
// end the sequence
|
||||
watch_buzzer_abort_sequence();
|
||||
if (_cb_finished) _cb_finished();
|
||||
}
|
||||
} else _tone_ticks--;
|
||||
}
|
||||
|
||||
void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end) {
|
||||
watch_buzzer_play_raw_source_with_volume(raw_source, userdata, callback_on_end, WATCH_BUZZER_VOLUME_LOUD);
|
||||
}
|
||||
|
||||
void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume) {
|
||||
// Abort any previous sequence
|
||||
watch_buzzer_abort_sequence();
|
||||
|
||||
if (_cb_start_global) {
|
||||
_cb_start_global();
|
||||
}
|
||||
|
||||
watch_enable_buzzer();
|
||||
|
||||
watch_set_buzzer_off();
|
||||
_raw_source = raw_source;
|
||||
_userdata = userdata;
|
||||
_cb_finished = callback_on_end;
|
||||
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
|
||||
_seq_position = 0;
|
||||
_tone_ticks = 0;
|
||||
// prepare buzzer
|
||||
|
||||
_cb_tc0 = cb_watch_buzzer_raw_source;
|
||||
// setup TC0 timer
|
||||
_tc0_initialize();
|
||||
// start the timer (for the 64 hz callback)
|
||||
_tc0_start();
|
||||
}
|
||||
|
||||
void cb_watch_buzzer_raw_source(void) {
|
||||
// callback for reading the note sequence
|
||||
uint16_t period;
|
||||
uint16_t duration;
|
||||
bool done;
|
||||
|
||||
if (_tone_ticks == 0) {
|
||||
done = _raw_source(_seq_position, _userdata, &period, &duration);
|
||||
|
||||
if (done || duration == 0) {
|
||||
// end the sequence
|
||||
watch_buzzer_abort_sequence();
|
||||
} else {
|
||||
if (period == WATCH_BUZZER_PERIOD_REST) {
|
||||
watch_set_buzzer_off();
|
||||
} else {
|
||||
watch_set_buzzer_period_and_duty_cycle(period, _volume);
|
||||
watch_set_buzzer_on();
|
||||
}
|
||||
|
||||
// set duration ticks and move to next tone
|
||||
_tone_ticks = duration - 1;
|
||||
_seq_position += 1;
|
||||
}
|
||||
} else {
|
||||
_tone_ticks--;
|
||||
}
|
||||
}
|
||||
|
||||
void watch_buzzer_abort_sequence(void) {
|
||||
// ends/aborts the sequence
|
||||
if (_callback_running) _tc0_stop();
|
||||
if (!_buzzer_is_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
_tc0_stop();
|
||||
|
||||
watch_set_buzzer_off();
|
||||
// disable standby mode for TCC
|
||||
_tcc_write_RUNSTDBY(false);
|
||||
|
||||
// disable TCC
|
||||
watch_disable_buzzer();
|
||||
|
||||
if (_cb_stop_global) {
|
||||
_cb_stop_global();
|
||||
}
|
||||
|
||||
if (_cb_finished) {
|
||||
_cb_finished();
|
||||
}
|
||||
}
|
||||
|
||||
void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop) {
|
||||
_cb_stop_global = cb_start;
|
||||
_cb_stop_global = cb_stop;
|
||||
}
|
||||
|
||||
void irq_handler_tc0(void) {
|
||||
// interrupt handler for TC0 (globally!)
|
||||
cb_watch_buzzer_seq();
|
||||
if (_cb_tc0) {
|
||||
_cb_tc0();
|
||||
}
|
||||
TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
|
||||
}
|
||||
|
||||
bool watch_is_buzzer_or_led_enabled(void){
|
||||
return tcc_is_enabled(0);
|
||||
void _watch_maybe_enable_tcc(void) {
|
||||
if (!_buzzer_is_active && !_led_is_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tcc_is_enabled(0)) {
|
||||
// tcc_set_run_in_standby(0, true);
|
||||
_watch_enable_tcc();
|
||||
// TCC should run in standby mode
|
||||
_tcc_write_RUNSTDBY(true);
|
||||
}
|
||||
}
|
||||
|
||||
inline void watch_enable_buzzer(void) {
|
||||
if (!tcc_is_enabled(0)) {
|
||||
_watch_enable_tcc();
|
||||
void _watch_maybe_disable_tcc(void) {
|
||||
if (_buzzer_is_active || _led_is_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tcc_is_enabled(0)) {
|
||||
_tcc_write_RUNSTDBY(false);
|
||||
_watch_disable_tcc();
|
||||
}
|
||||
}
|
||||
|
||||
void watch_enable_buzzer(void) {
|
||||
_buzzer_is_active = true;
|
||||
_watch_maybe_enable_tcc();
|
||||
}
|
||||
|
||||
void watch_disable_buzzer(void) {
|
||||
_buzzer_is_active = false;
|
||||
watch_set_buzzer_off();
|
||||
_watch_maybe_disable_tcc();
|
||||
}
|
||||
|
||||
void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) {
|
||||
tcc_set_period(0, period, true);
|
||||
tcc_set_cc(0, (WATCH_BUZZER_TCC_CHANNEL) % 4, period / (100 / duty), true);
|
||||
}
|
||||
|
||||
void watch_disable_buzzer(void) {
|
||||
_watch_disable_tcc();
|
||||
// The buzzer determines the period, which means that if the LED was active before it will flicker
|
||||
// Update the LED duty cycle to match the new period required by the buzzer.
|
||||
if (_led_is_active) {
|
||||
_watch_set_led_duty_cycle(period, _current_led_color[0], _current_led_color[1], _current_led_color[2]);
|
||||
}
|
||||
}
|
||||
|
||||
inline void watch_set_buzzer_on(void) {
|
||||
@@ -172,14 +302,17 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms) {
|
||||
}
|
||||
|
||||
void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) {
|
||||
if (note == BUZZER_NOTE_REST) {
|
||||
watch_set_buzzer_off();
|
||||
} else {
|
||||
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25);
|
||||
watch_set_buzzer_on();
|
||||
}
|
||||
delay_ms(duration_ms);
|
||||
watch_set_buzzer_off();
|
||||
static int8_t single_note_sequence[3];
|
||||
|
||||
single_note_sequence[0] = note;
|
||||
// 64 ticks per second for the tc0
|
||||
// Each tick is approximately 15ms
|
||||
uint16_t duration = duration_ms / 15;
|
||||
if (duration > 127) duration = 127;
|
||||
single_note_sequence[1] = (int8_t)duration;
|
||||
single_note_sequence[2] = 0;
|
||||
|
||||
watch_buzzer_play_sequence_with_volume(single_note_sequence, NULL, volume);
|
||||
}
|
||||
|
||||
void _watch_enable_tcc(void) {
|
||||
@@ -220,21 +353,6 @@ void _watch_enable_tcc(void) {
|
||||
tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, 0, false);
|
||||
#endif
|
||||
|
||||
// enable LED PWM pins (the LED driver assumes if the TCC is on, the pins are enabled)
|
||||
HAL_GPIO_RED_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
|
||||
HAL_GPIO_RED_drvstr(1);
|
||||
HAL_GPIO_RED_out();
|
||||
#ifdef WATCH_GREEN_TCC_CHANNEL
|
||||
HAL_GPIO_GREEN_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
|
||||
HAL_GPIO_GREEN_drvstr(1);
|
||||
HAL_GPIO_GREEN_out();
|
||||
#endif
|
||||
#ifdef WATCH_BLUE_TCC_CHANNEL
|
||||
HAL_GPIO_BLUE_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
|
||||
HAL_GPIO_BLUE_drvstr(1);
|
||||
HAL_GPIO_BLUE_out();
|
||||
#endif
|
||||
|
||||
// Enable the TCC
|
||||
tcc_enable(0);
|
||||
}
|
||||
@@ -257,13 +375,45 @@ void _watch_disable_tcc(void) {
|
||||
}
|
||||
|
||||
void watch_enable_leds(void) {
|
||||
if (!tcc_is_enabled(0)) {
|
||||
_watch_enable_tcc();
|
||||
}
|
||||
_led_is_active = true;
|
||||
_watch_enable_led_pins();
|
||||
_watch_maybe_enable_tcc();
|
||||
}
|
||||
|
||||
void watch_disable_leds(void) {
|
||||
_watch_disable_tcc();
|
||||
_led_is_active = false;
|
||||
_watch_disable_led_pins();
|
||||
_watch_maybe_disable_tcc();
|
||||
}
|
||||
|
||||
void _watch_enable_led_pins(void) {
|
||||
// enable LED PWM pins (the LED driver assumes if the TCC is on, the pins are enabled)
|
||||
HAL_GPIO_RED_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
|
||||
HAL_GPIO_RED_drvstr(1);
|
||||
HAL_GPIO_RED_out();
|
||||
#ifdef WATCH_GREEN_TCC_CHANNEL
|
||||
HAL_GPIO_GREEN_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
|
||||
HAL_GPIO_GREEN_drvstr(1);
|
||||
HAL_GPIO_GREEN_out();
|
||||
#endif
|
||||
#ifdef WATCH_BLUE_TCC_CHANNEL
|
||||
HAL_GPIO_BLUE_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
|
||||
HAL_GPIO_BLUE_drvstr(1);
|
||||
HAL_GPIO_BLUE_out();
|
||||
#endif
|
||||
}
|
||||
|
||||
void _watch_disable_led_pins(void) {
|
||||
HAL_GPIO_RED_pmuxdis();
|
||||
HAL_GPIO_RED_off();
|
||||
#ifdef WATCH_GREEN_TCC_CHANNEL
|
||||
HAL_GPIO_GREEN_pmuxdis();
|
||||
HAL_GPIO_GREEN_off();
|
||||
#endif
|
||||
#ifdef WATCH_BLUE_TCC_CHANNEL
|
||||
HAL_GPIO_BLUE_pmuxdis();
|
||||
HAL_GPIO_BLUE_off();
|
||||
#endif
|
||||
}
|
||||
|
||||
void watch_set_led_color(uint8_t red, uint8_t green) {
|
||||
@@ -274,9 +424,7 @@ void watch_set_led_color(uint8_t red, uint8_t green) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) {
|
||||
if (tcc_is_enabled(0)) {
|
||||
uint32_t period = tcc_get_period(0);
|
||||
static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue) {
|
||||
tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true);
|
||||
#ifdef WATCH_GREEN_TCC_CHANNEL
|
||||
tcc_set_cc(0, (WATCH_GREEN_TCC_CHANNEL) % 4, ((period * (uint32_t)green * 1000ull) / 255000ull), true);
|
||||
@@ -288,6 +436,23 @@ void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) {
|
||||
#else
|
||||
(void) blue; // silence warning
|
||||
#endif
|
||||
}
|
||||
|
||||
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) {
|
||||
bool turning_on = (red | green | blue) != 0;
|
||||
|
||||
if (turning_on) {
|
||||
_current_led_color[0] = red;
|
||||
_current_led_color[1] = green;
|
||||
_current_led_color[2] = blue;
|
||||
watch_enable_leds();
|
||||
uint32_t period = tcc_get_period(0);
|
||||
_watch_set_led_duty_cycle(period, red, green, blue);
|
||||
} else {
|
||||
if (tcc_is_enabled(0)) {
|
||||
_watch_set_led_duty_cycle(1, red, green, blue);
|
||||
}
|
||||
watch_disable_leds();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -277,20 +277,26 @@ inline void lis2dw_disable_fifo(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data) {
|
||||
bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data, uint32_t timeout) {
|
||||
// timeout is in terms of 1/RTC_CNT_HZ seconds (likely 128 timeouts is one second)
|
||||
#ifdef I2C_SERCOM
|
||||
uint8_t temp = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_FIFO_SAMPLE);
|
||||
bool overrun = !!(temp & LIS2DW_FIFO_SAMPLE_OVERRUN);
|
||||
|
||||
fifo_data->count = temp & LIS2DW_FIFO_SAMPLE_COUNT;
|
||||
|
||||
rtc_counter_t timeout_counter = watch_rtc_get_counter() + timeout;
|
||||
for(int i = 0; i < fifo_data->count; i++) {
|
||||
if (watch_rtc_get_counter() > timeout_counter) {
|
||||
break;
|
||||
}
|
||||
fifo_data->readings[i] = lis2dw_get_raw_reading();
|
||||
}
|
||||
|
||||
return overrun;
|
||||
#else
|
||||
(void) fifo_data;
|
||||
(void) timeout;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
@@ -302,6 +308,20 @@ void lis2dw_clear_fifo(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void lis2dw_enable_double_tap(void) {
|
||||
#ifdef I2C_SERCOM
|
||||
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS);
|
||||
watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS, configuration | LIS2DW_WAKE_UP_THS_ENABLE_DOUBLE_TAP);
|
||||
#endif
|
||||
}
|
||||
|
||||
void lis2dw_disable_double_tap(void) {
|
||||
#ifdef I2C_SERCOM
|
||||
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS);
|
||||
watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS, configuration & ~LIS2DW_WAKE_UP_THS_ENABLE_DOUBLE_TAP);
|
||||
#endif
|
||||
}
|
||||
|
||||
void lis2dw_enable_sleep(void) {
|
||||
#ifdef I2C_SERCOM
|
||||
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS);
|
||||
@@ -397,6 +417,33 @@ void lis2dw_configure_int2(uint8_t sources) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void lis2dw12_int_notification_set(lis2dw12_lir_t val) {
|
||||
#ifdef I2C_SERCOM
|
||||
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3);
|
||||
if (val == LIS2DW12_INT_LATCHED) {
|
||||
configuration |= LIS2DW_CTRL3_VAL_LIR;
|
||||
} else {
|
||||
configuration &= ~LIS2DW_CTRL7_VAL_DRDY_PULSED;
|
||||
}
|
||||
watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3, configuration);
|
||||
#else
|
||||
(void)val;
|
||||
#endif
|
||||
}
|
||||
|
||||
lis2dw12_lir_t lis2dw12_int_notification_get(void) {
|
||||
#ifdef I2C_SERCOM
|
||||
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3);
|
||||
if (configuration & LIS2DW12_INT_LATCHED) {
|
||||
return LIS2DW12_INT_LATCHED;
|
||||
} else {
|
||||
return LIS2DW12_INT_PULSED;
|
||||
}
|
||||
#else
|
||||
return LIS2DW12_INT_PULSED;
|
||||
#endif
|
||||
}
|
||||
|
||||
void lis2dw_enable_interrupts(void) {
|
||||
#ifdef I2C_SERCOM
|
||||
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7);
|
||||
@@ -411,6 +458,20 @@ void lis2dw_disable_interrupts(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void lis2dw_pulsed_drdy_interrupts(void) {
|
||||
#ifdef I2C_SERCOM
|
||||
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7);
|
||||
watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration | LIS2DW_CTRL7_VAL_DRDY_PULSED);
|
||||
#endif
|
||||
}
|
||||
|
||||
void lis2dw_latched_drdy_interrupts(void) {
|
||||
#ifdef I2C_SERCOM
|
||||
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7);
|
||||
watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration & ~LIS2DW_CTRL7_VAL_DRDY_PULSED);
|
||||
#endif
|
||||
}
|
||||
|
||||
lis2dw_wakeup_source_t lis2dw_get_wakeup_source() {
|
||||
#ifdef I2C_SERCOM
|
||||
return (lis2dw_wakeup_source_t) watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_SRC);
|
||||
|
||||
@@ -92,6 +92,12 @@ typedef enum {
|
||||
LIS2DW_FILTER_HIGH_PASS = 1,
|
||||
} lis2dw_filter_t;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
LIS2DW12_INT_PULSED = 0,
|
||||
LIS2DW12_INT_LATCHED = 1,
|
||||
} lis2dw12_lir_t;
|
||||
|
||||
typedef enum {
|
||||
LIS2DW_RANGE_16_G = 0b11, // +/- 16g
|
||||
LIS2DW_RANGE_8_G = 0b10, // +/- 8g
|
||||
@@ -295,6 +301,8 @@ typedef enum {
|
||||
#define LIS2DW_CTRL7_VAL_HP_REF_MODE 0b00000010
|
||||
#define LIS2DW_CTRL7_VAL_LPASS_ON6D 0b00000001
|
||||
|
||||
#define LIS2DW_FIFO_TIMEOUT 100 // timeout is in terms of 1/RTC_CNT_HZ seconds (likely 128 timeouts is one second)
|
||||
|
||||
bool lis2dw_begin(void);
|
||||
|
||||
uint8_t lis2dw_get_device_id(void);
|
||||
@@ -339,10 +347,14 @@ void lis2dw_enable_fifo(void);
|
||||
|
||||
void lis2dw_disable_fifo(void);
|
||||
|
||||
bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data);
|
||||
bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data, uint32_t timeout);
|
||||
|
||||
void lis2dw_clear_fifo(void);
|
||||
|
||||
void lis2dw_enable_double_tap(void);
|
||||
|
||||
void lis2dw_disable_double_tap(void);
|
||||
|
||||
void lis2dw_enable_sleep(void);
|
||||
|
||||
void lis2dw_disable_sleep(void);
|
||||
@@ -363,6 +375,10 @@ void lis2dw_configure_tap_threshold(uint8_t threshold_x, uint8_t threshold_y, ui
|
||||
|
||||
void lis2dw_configure_tap_duration(uint8_t latency, uint8_t quiet, uint8_t shock);
|
||||
|
||||
void lis2dw12_int_notification_set(lis2dw12_lir_t val);
|
||||
|
||||
lis2dw12_lir_t lis2dw12_int_notification_get(void);
|
||||
|
||||
void lis2dw_configure_int1(uint8_t sources);
|
||||
|
||||
void lis2dw_configure_int2(uint8_t sources);
|
||||
@@ -371,6 +387,10 @@ void lis2dw_enable_interrupts(void);
|
||||
|
||||
void lis2dw_disable_interrupts(void);
|
||||
|
||||
void lis2dw_pulsed_drdy_interrupts(void);
|
||||
|
||||
void lis2dw_latched_drdy_interrupts(void);
|
||||
|
||||
lis2dw_interrupt_source_t lis2dw_get_interrupt_source(void);
|
||||
|
||||
lis2dw_wakeup_source_t lis2dw_get_wakeup_source(void);
|
||||
|
||||
@@ -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);
|
||||
|
||||
/** @} */
|
||||
@@ -45,22 +45,25 @@ void watch_disable_i2c(void);
|
||||
* @param addr The address of the device you wish to talk to.
|
||||
* @param buf A series of unsigned bytes; the data you wish to transmit.
|
||||
* @param length The number of bytes in buf that you wish to send.
|
||||
* @return 0 if no error code, otherwise a code via i2c_result_t
|
||||
*/
|
||||
void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length);
|
||||
int8_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length);
|
||||
|
||||
/** @brief Receives a series of values from a device on the I2C bus.
|
||||
* @param addr The address of the device you wish to hear from.
|
||||
* @param buf Storage for the incoming bytes; on return, it will contain the received data.
|
||||
* @param length The number of bytes that you wish to receive.
|
||||
* @return 0 if no error code, otherwise a code via i2c_result_t
|
||||
*/
|
||||
void watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length);
|
||||
int8_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length);
|
||||
|
||||
/** @brief Writes a byte to a register in an I2C device.
|
||||
* @param addr The address of the device you wish to address.
|
||||
* @param reg The register on the device that you wish to set.
|
||||
* @param data The value that you wish to set the register to.
|
||||
* @return 0 if no error code, otherwise a code via i2c_result_t
|
||||
*/
|
||||
void watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data);
|
||||
int8_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data);
|
||||
|
||||
/** @brief Reads a byte from a register in an I2C device.
|
||||
* @param addr The address of the device you wish to address.
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
////< @file watch_rtc.h
|
||||
|
||||
#include "watch.h"
|
||||
#include "rtc.h"
|
||||
#include "rtc32.h"
|
||||
|
||||
/** @addtogroup rtc Real-Time Clock
|
||||
* @brief This section covers functions related to the SAM L22's real-time clock peripheral, including
|
||||
@@ -42,17 +42,20 @@
|
||||
extern watch_cb_t btn_alarm_callback;
|
||||
extern watch_cb_t a2_callback;
|
||||
extern watch_cb_t a4_callback;
|
||||
extern watch_cb_t comp_callback;
|
||||
|
||||
#define WATCH_RTC_REFERENCE_YEAR (2020)
|
||||
|
||||
#define watch_date_time_t rtc_date_time_t
|
||||
typedef rtc_counter_t watch_counter_t;
|
||||
typedef uint32_t unix_timestamp_t;
|
||||
|
||||
/** @brief Called by main.c to check if the RTC is enabled.
|
||||
* You may call this function, but outside of app_init, it should always return true.
|
||||
*/
|
||||
bool _watch_rtc_is_enabled(void);
|
||||
|
||||
/** @brief Sets the date and time.
|
||||
/** @brief Sets the date and time. Calls watch_rtc_set_unix_time internally.
|
||||
* @param date_time The date and time you wish to set, with a year value from 0-63 representing 2020-2083.
|
||||
* @note The SAM L22 stores the year as six bits representing a value from 0 to 63. It treats this as a year
|
||||
* offset from a reference year, which must be a leap year. Since 2020 was a leap year, and it allows
|
||||
@@ -62,7 +65,7 @@ bool _watch_rtc_is_enabled(void);
|
||||
*/
|
||||
void watch_rtc_set_date_time(rtc_date_time_t date_time);
|
||||
|
||||
/** @brief Returns the date and time.
|
||||
/** @brief Returns the date and time. Calls watch_rtc_get_unix_time internally.
|
||||
* @return A rtc_date_time_t with the current date and time, with a year value from 0-63 representing 2020-2083.
|
||||
* @see watch_rtc_set_date_time for notes about how the year is stored.
|
||||
*/
|
||||
@@ -73,26 +76,79 @@ rtc_date_time_t watch_rtc_get_date_time(void);
|
||||
*/
|
||||
rtc_date_time_t watch_get_init_date_time(void);
|
||||
|
||||
/** @brief Registers an alarm callback that will be called when the RTC time matches the target time, as masked
|
||||
* by the provided mask.
|
||||
* @param callback The function you wish to have called when the alarm fires. If this value is NULL, the alarm
|
||||
* interrupt will still be enabled, but no callback function will be called.
|
||||
* @param alarm_time The time that you wish to match. The date is currently ignored.
|
||||
* @param mask One of the values in rtc_alarm_match_t indicating which values to check.
|
||||
* @details The alarm interrupt is a versatile tool for scheduling events in the future, especially since it can
|
||||
* wake the device from all sleep modes. The key to its versatility is the mask parameter.
|
||||
* Suppose we set an alarm for midnight, 00:00:00.
|
||||
* * if mask is ALARM_MATCH_SS, the alarm will fire every minute when the clock ticks to seconds == 0.
|
||||
* * with ALARM_MATCH_MMSS, the alarm will once an hour, at the top of each hour.
|
||||
* * with ALARM_MATCH_HHMMSS, the alarm will fire at midnight every day.
|
||||
* In theory the SAM L22's alarm function can match on days, months and even years, but I have not had
|
||||
* success with this yet; as such, I am omitting these options for now.
|
||||
/** @brief Set the current UTC date and time using a unix timestamp
|
||||
*/
|
||||
void watch_rtc_register_alarm_callback(watch_cb_t callback, rtc_date_time_t alarm_time, rtc_alarm_match_t mask);
|
||||
void watch_rtc_set_unix_time(unix_timestamp_t unix_time);
|
||||
|
||||
/** @brief Get the current UTC date and time using a unix timestamp
|
||||
*/
|
||||
unix_timestamp_t watch_rtc_get_unix_time(void);
|
||||
|
||||
/** @brief Get the current value of the internal hardware counter
|
||||
* @details The counter starts at 0 and it increases at a 128Hz rate until it overflows and starts over.
|
||||
* We never manually set the counter. Doing so allows us to calculate absolute elapsed and more.
|
||||
* When the user sets the time, what is modified is the reference time (i.e. the date and time when
|
||||
* the counter is 0).
|
||||
*/
|
||||
rtc_counter_t watch_rtc_get_counter(void);
|
||||
|
||||
/** @brief Get the RTC counter frequency.
|
||||
*/
|
||||
uint32_t watch_rtc_get_frequency(void);
|
||||
|
||||
/** @brief Get how many counter ticks are in one minute.
|
||||
*/
|
||||
uint32_t watch_rtc_get_ticks_per_minute(void);
|
||||
|
||||
/** @brief Registers a callback that will be called when the RTC counter matches the target counter.
|
||||
* @param callback The function you wish to have called when the target counter is reached. If this value is NULL, the comp
|
||||
* interrupt will still be enabled, but no callback function will be called.
|
||||
* @param counter The time that you wish to match. The date is currently ignored.
|
||||
* @param index We can have up to 8 active callbacks at a time. This parameter specifies which of the 8 callbacks should be set.
|
||||
* @details The hardware RTC provides us with single interrupt that fires when the RTC counter matches a target counter COMP0.
|
||||
* With a little bit of logic, we can provide multiple active compare callbacks. Every time a comp callback is
|
||||
* registered/disabled/fired we iterate over all the active comp callbacks and set the hardware COMP0 counter
|
||||
* to the next occurring one.
|
||||
* With this very simple API, movement can implement one-shot timers to turn off the led and determine button longpresses
|
||||
* as well as the inactivity timeouts for resigning and sleeping, as well as emulating the top of the minute alarm.
|
||||
*/
|
||||
void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index);
|
||||
|
||||
/** @brief Just like watch_rtc_register_comp_callback but doesn't actually schedule the callback
|
||||
*
|
||||
* Useful if you need register multiple callbacks at once, avoids multiple calls to the expensive watch_rtc_schedule_next_comp:
|
||||
* Usage:
|
||||
* watch_rtc_register_comp_callback_no_schedule(cb0, counter0, index0);
|
||||
* watch_rtc_register_comp_callback_no_schedule(cb1, counter1, index1);
|
||||
* watch_rtc_schedule_next_comp();
|
||||
*/
|
||||
void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index);
|
||||
|
||||
/** @brief Disables the specified comp callback.
|
||||
*/
|
||||
void watch_rtc_disable_comp_callback(uint8_t index);
|
||||
|
||||
/** @brief Just like watch_rtc_disable_comp_callback but doesn't actually schedule the callback
|
||||
*
|
||||
* Useful if you need disable multiple callbacks at once, avoids multiple calls to the expensive watch_rtc_schedule_next_comp:
|
||||
* Usage:
|
||||
* watch_rtc_disable_comp_callback_no_schedule(index0);
|
||||
* watch_rtc_disable_comp_callback_no_schedule(index1);
|
||||
* watch_rtc_schedule_next_comp();
|
||||
*/
|
||||
/** @brief Disables the specified comp callback.
|
||||
*/
|
||||
void watch_rtc_disable_comp_callback_no_schedule(uint8_t index);
|
||||
|
||||
/** @brief Determines the first comp callback that should fire and schedule it with the RTC
|
||||
*
|
||||
* You would never need to call this manually, unless you used the 'no_schedule' functions above.
|
||||
*/
|
||||
void watch_rtc_schedule_next_comp(void);
|
||||
|
||||
/** @brief Disables the alarm callback.
|
||||
*/
|
||||
void watch_rtc_disable_alarm_callback(void);
|
||||
// void watch_rtc_disable_alarm_callback(void);
|
||||
|
||||
/** @brief Registers a "tick" callback that will be called once per second.
|
||||
* @param callback The function you wish to have called when the clock ticks. If you pass in NULL, the tick
|
||||
@@ -117,10 +173,6 @@ void watch_rtc_disable_tick_callback(void);
|
||||
* tick at 16 or 32 Hz to update the screen more quickly. Just remember that the more frequent the tick, the more
|
||||
* power your app will consume. Ideally you should enable the fast tick only when the user requires it (i.e. in
|
||||
* response to an input event), and move back to the slow tick after some time.
|
||||
*
|
||||
* Also note that the RTC peripheral does not have sub-second resolution, so even if you set a 2 or 4 Hz interval,
|
||||
* the system will not have any way of telling you where you are within a given second; watch_rtc_get_date_time
|
||||
* will return the exact same timestamp until the second ticks over.
|
||||
*/
|
||||
void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency);
|
||||
|
||||
|
||||
@@ -98,6 +98,10 @@ typedef enum {
|
||||
*/
|
||||
void watch_enable_display(void);
|
||||
|
||||
/** @brief Disables the Segment LCD display.
|
||||
*/
|
||||
void watch_disable_display(void);
|
||||
|
||||
/** @brief Sets a pixel. Use this to manually set a pixel with a given common and segment number.
|
||||
* See <a href="segmap.html">segmap.html</a>.
|
||||
* @param com the common pin, numbered from 0-2.
|
||||
|
||||
@@ -126,12 +126,9 @@ typedef enum {
|
||||
BUZZER_NOTE_REST ///< no sound
|
||||
} watch_buzzer_note_t;
|
||||
|
||||
/** @brief Returns true if either the buzzer or the LED driver is enabled.
|
||||
* @details Both the buzzer and the LED use the TCC peripheral to drive their behavior. This function returns true if that
|
||||
* peripheral is enabled. You can use this function to determine whether you need to call the watch_enable_leds or
|
||||
* or watch_enable_buzzer functions before using these peripherals.
|
||||
*/
|
||||
bool watch_is_buzzer_or_led_enabled(void);
|
||||
#define WATCH_BUZZER_PERIOD_REST 0
|
||||
|
||||
typedef bool (*watch_buzzer_raw_source_t)(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration);
|
||||
|
||||
/** @addtogroup tcc Buzzer and LED Control (via the TCC peripheral)
|
||||
* @brief This section covers functions related to Timer Counter for Control peripheral, which drives the piezo buzzer
|
||||
@@ -151,15 +148,13 @@ void watch_enable_buzzer(void);
|
||||
*/
|
||||
void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty);
|
||||
|
||||
/** @brief Disables the TCC peripheral that drives the buzzer.
|
||||
* @note If you are using PWM to set custom LED colors, this method will also disable the LED PWM driver,
|
||||
* since the buzzer and LED both make use of the same peripheral to drive their PWM behavior.
|
||||
/** @brief Disables the TCC peripheral that drives the buzzer (if LED not active).
|
||||
*/
|
||||
void watch_disable_buzzer(void);
|
||||
|
||||
/** @brief Turns the buzzer output on. It will emit a continuous sound at the given frequency.
|
||||
* @note The TCC peripheral that drives the buzzer does not run in standby mode; if you wish for buzzer
|
||||
* output to continue, you should prevent your app from going to sleep.
|
||||
* @note The TCC peripheral that drives the buzzer does run in standby mode; if you wish for buzzer
|
||||
* output to continue, you don't need to prevent your app from going to sleep.
|
||||
*/
|
||||
void watch_set_buzzer_on(void);
|
||||
|
||||
@@ -170,8 +165,6 @@ void watch_set_buzzer_off(void);
|
||||
/** @brief Plays the given note for a set duration at the loudest possible volume.
|
||||
* @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration.
|
||||
* @param duration_ms The duration of the note.
|
||||
* @note Note that this will block your UI for the duration of the note's play time, and it will
|
||||
* after this call, the buzzer period will be set to the period of this note.
|
||||
*/
|
||||
void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms);
|
||||
|
||||
@@ -179,8 +172,6 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms);
|
||||
* @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration.
|
||||
* @param duration_ms The duration of the note.
|
||||
* @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD
|
||||
* @note This will block your UI for the duration of the note's play time, and after this call, the
|
||||
* buzzer will stop sounding, but the TCC period will remain set to the period of this note.
|
||||
*/
|
||||
void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume);
|
||||
|
||||
@@ -202,10 +193,69 @@ extern const uint16_t NotePeriods[108];
|
||||
*/
|
||||
void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void));
|
||||
|
||||
/** @brief Plays the given sequence of notes in a non-blocking way.
|
||||
* @param note_sequence A pointer to the sequence of buzzer note & duration tuples, ending with a zero. A simple
|
||||
* RLE logic is implemented: a negative number instead of a buzzer note means that the sequence
|
||||
* is rewound by the given number of notes. The byte following a negative number determines the number
|
||||
* of loops. I.e. if you want to repeat the last three notes of the sequence one time, you should provide
|
||||
* the tuple -3, 1. The repeated notes must not contain any other repeat markers, or you will end up with
|
||||
* an eternal loop.
|
||||
* @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing.
|
||||
* @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD
|
||||
*/
|
||||
void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume);
|
||||
|
||||
/** @brief Plays the given raw buzzer source function in a non-blocking way.
|
||||
*
|
||||
* @details This function plays audio data generated by a raw source callback function,
|
||||
* allowing for precise control over buzzer timing and frequency. The raw source
|
||||
* function is called repeatedly to generate audio samples, each containing a
|
||||
* period and duration for the buzzer tone.
|
||||
* Useful for applications such as chirpy, so that they won't need to allocate a
|
||||
* long note sequence, and we will also take care of all the timing logic.
|
||||
*
|
||||
* @param raw_source Pointer to the callback function that generates raw buzzer data.
|
||||
* The function should take a position parameter and return true if
|
||||
* more data is available, false if end of sequence is reached.
|
||||
* Parameters:
|
||||
* - position: Current position in the audio sequence (0-based)
|
||||
* - userdata: User-provided data passed through to the callback
|
||||
* - period: Pointer to store the period (in microseconds) for the tone
|
||||
* - duration: Pointer to store the duration (in microseconds) for the tone
|
||||
* @param userdata Pointer to user data that will be passed to the raw_source callback
|
||||
* @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing.
|
||||
*/
|
||||
void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end);
|
||||
|
||||
/** @brief Plays the given raw buzzer source function in a non-blocking way.
|
||||
*
|
||||
* @details This function plays audio data generated by a raw source callback function,
|
||||
* allowing for precise control over buzzer timing and frequency. The raw source
|
||||
* function is called repeatedly to generate audio samples, each containing a
|
||||
* period and duration for the buzzer tone.
|
||||
* Useful for applications such as chirpy, so that they won't need to allocate a
|
||||
* long note sequence, and we will also take care of all the timing logic.
|
||||
*
|
||||
* @param raw_source Pointer to the callback function that generates raw buzzer data.
|
||||
* The function should take a position parameter and return true if
|
||||
* more data is available, false if end of sequence is reached.
|
||||
* Parameters:
|
||||
* - position: Current position in the audio sequence (0-based)
|
||||
* - userdata: User-provided data passed through to the callback
|
||||
* - period: Pointer to store the period (in microseconds) for the tone
|
||||
* - duration: Pointer to store the duration (in microseconds) for the tone
|
||||
* @param userdata Pointer to user data that will be passed to the raw_source callback
|
||||
* @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing.
|
||||
* @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD
|
||||
*/
|
||||
void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume);
|
||||
|
||||
/** @brief Aborts a playing sequence.
|
||||
*/
|
||||
void watch_buzzer_abort_sequence(void);
|
||||
|
||||
void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop);
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
void irq_handler_tc0(void);
|
||||
#endif
|
||||
@@ -223,26 +273,17 @@ void irq_handler_tc0(void);
|
||||
* so that watch_set_led_red sets the red LED, and watch_set_led_green sets the blue one.
|
||||
*/
|
||||
/// @{
|
||||
/** @brief Enables the bi-color LED.
|
||||
* @note The TCC peripheral that drives the LEDs does not run in STANDBY mode — but the outputs do! This
|
||||
* means that if you set either red, green or both LEDs to full power, they will shine even when
|
||||
* your app is asleep. If, however, you set a custom color using watch_set_led_color, the color will
|
||||
* not display correctly in STANDBY mode. You will need to keep your app running while the LED is on.
|
||||
/** @brief Enables the TCC peripheral, which drives the LEDs.
|
||||
*/
|
||||
void watch_enable_leds(void);
|
||||
|
||||
/** @brief Disables the LEDs.
|
||||
* @note This method will also disable the buzzer, since the buzzer and LED both make use of the same
|
||||
* peripheral to drive their PWM behavior.
|
||||
/** @brief Disables the TCC peripheral that drives the LEDs (if buzzer not active).
|
||||
*/
|
||||
void watch_disable_leds(void);
|
||||
|
||||
/** @brief Sets the LED to a custom color by modulating each output's duty cycle.
|
||||
* @param red The red value from 0-255.
|
||||
* @param green The green value from 0-255. If your watch has a red/blue LED, this will be the blue value.
|
||||
* @note If you are displaying a custom color, you will need to prevent your app from going to sleep
|
||||
* while the LED is on; otherwise, the color will not display correctly. You can do this by
|
||||
* returning false in your app_loop method.
|
||||
*/
|
||||
void watch_set_led_color(uint8_t red, uint8_t green);
|
||||
|
||||
@@ -250,9 +291,6 @@ void watch_set_led_color(uint8_t red, uint8_t green);
|
||||
* @param red The red value from 0-255.
|
||||
* @param green The green value from 0-255.
|
||||
* @param blue The blue value from 0-255.
|
||||
* @note If you are displaying a custom color, you will need to prevent your app from going to sleep
|
||||
* while the LED is on; otherwise, the color will not display correctly. You can do this by
|
||||
* returning false in your app_loop method.
|
||||
*/
|
||||
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue);
|
||||
|
||||
@@ -277,9 +315,6 @@ void watch_set_led_yellow(void);
|
||||
/** @brief Turns both the red and the green LEDs off. */
|
||||
void watch_set_led_off(void);
|
||||
|
||||
/** @brief Disables the TCC peripheral. Should only be called internally. */
|
||||
void _watch_disable_tcc(void);
|
||||
|
||||
/// @brief An array of periods for all the notes on a piano, corresponding to the names in watch_buzzer_note_t.
|
||||
extern const uint16_t NotePeriods[108];
|
||||
|
||||
|
||||
@@ -205,7 +205,8 @@ uint32_t watch_utility_date_time_to_unix_time(watch_date_time_t date_time, int32
|
||||
watch_date_time_t watch_utility_date_time_from_unix_time(uint32_t timestamp, int32_t utc_offset) {
|
||||
watch_date_time_t retval;
|
||||
retval.reg = 0;
|
||||
int32_t days, secs;
|
||||
uint32_t secs;
|
||||
int32_t days;
|
||||
int32_t remdays, remsecs, remyears;
|
||||
int32_t qc_cycles, c_cycles, q_cycles;
|
||||
int32_t years, months;
|
||||
@@ -277,6 +278,10 @@ watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_ti
|
||||
return watch_utility_date_time_from_unix_time(timestamp, destination_utc_offset);
|
||||
}
|
||||
|
||||
uint32_t watch_utility_unix_time_convert_zone(uint32_t timestamp, uint32_t origin_utc_offset, uint32_t destination_utc_offset) {
|
||||
return timestamp - origin_utc_offset + destination_utc_offset;
|
||||
}
|
||||
|
||||
watch_duration_t watch_utility_seconds_to_duration(uint32_t seconds) {
|
||||
watch_duration_t retval;
|
||||
|
||||
|
||||
@@ -144,6 +144,16 @@ bool watch_utility_convert_to_12_hour(watch_date_time_t *date_time);
|
||||
*/
|
||||
watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_time, uint32_t origin_utc_offset, uint32_t destination_utc_offset);
|
||||
|
||||
/** @brief Converts a unix time from a given time zone to another time zone.
|
||||
* @param timestamp The unix time that you wish to convert
|
||||
* @param origin_utc_offset The number of seconds from UTC in the origin time zone
|
||||
* @param destination_utc_offset The number of seconds from UTC in the destination time zone
|
||||
* @return A unix time for the given UNIX timestamp and UTC offset.
|
||||
* @note Adapted from MIT-licensed code from musl, Copyright © 2005-2014 Rich Felker, et al.:
|
||||
* https://github.com/esmil/musl/blob/1cc81f5cb0df2b66a795ff0c26d7bbc4d16e13c6/src/time/__secs_to_tm.c
|
||||
*/
|
||||
uint32_t watch_utility_unix_time_convert_zone(uint32_t timestamp, uint32_t origin_utc_offset, uint32_t destination_utc_offset);
|
||||
|
||||
/** @brief Returns a temperature in degrees Celsius for a given thermistor voltage divider circuit.
|
||||
* @param value The raw analog reading from the thermistor pin (0-65535)
|
||||
* @param highside True if the thermistor is connected to VCC and the series resistor is connected
|
||||
|
||||
@@ -64,21 +64,21 @@
|
||||
<h1 style="text-align: center;">Sensor Watch Emulator</h1>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1271 1311" width="320">
|
||||
<defs>
|
||||
<radialGradient id="Dégradé_sans_nom_3" data-name="Dégradé sans nom 3" cx="635" cy="687" fx="260.1751499985994" r="374.83" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.28" stop-color="lime"/>
|
||||
<stop offset="0.46" stop-color="#00b200" stop-opacity="0.7"/>
|
||||
<stop offset="0.65" stop-color="#006700" stop-opacity="0.4"/>
|
||||
<stop offset="0.82" stop-color="#002f00" stop-opacity="0.19"/>
|
||||
<stop offset="0.94" stop-color="#000d00" stop-opacity="0.05"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
<radialGradient id="Dégradé_sans_nom_3" data-name="Dégradé sans nom 3"
|
||||
cx="635" cy="687" fx="260.1751499985994" r="374.83" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.28" stop-color="white"/>
|
||||
<stop offset="0.46" stop-color="#ccc" stop-opacity="0.7"/>
|
||||
<stop offset="0.65" stop-color="#999" stop-opacity="0.4"/>
|
||||
<stop offset="0.82" stop-color="#666" stop-opacity="0.19"/>
|
||||
<stop offset="0.94" stop-color="#333" stop-opacity="0.05"/>
|
||||
<stop offset="1" stop-color="#000" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<filter id="ledcolor">
|
||||
<feColorMatrix in="SourceGraphic" type="matrix"
|
||||
values=" 0 0 0 0 0
|
||||
values="1 0 0 0 0
|
||||
0 1 0 0 0
|
||||
0 0 0 0 0
|
||||
0 0 0 1 0 "/>
|
||||
|
||||
0 0 1 0 0
|
||||
0 0 0 1 0"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g id="Calque">
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
#include "watch.h"
|
||||
|
||||
bool watch_is_buzzer_or_led_enabled(void) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool watch_is_usb_enabled(void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -28,11 +28,17 @@ void watch_enable_i2c(void) {}
|
||||
|
||||
void watch_disable_i2c(void) {}
|
||||
|
||||
void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) {}
|
||||
int8_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) {}
|
||||
int8_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) {}
|
||||
int8_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) {
|
||||
return 0;
|
||||
|
||||
@@ -51,8 +51,6 @@ int _gettimeofday(struct timeval *tv, void *tzvp) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void _watch_disable_tcc(void) {}
|
||||
|
||||
void _watch_enable_usb(void) {}
|
||||
|
||||
void watch_disable_TRNG() {}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "watch_rtc.h"
|
||||
#include "watch_main_loop.h"
|
||||
@@ -29,8 +31,29 @@
|
||||
#include <emscripten.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 long tick_callbacks[8] = { -1, -1, -1, -1, -1, -1, -1, -1 };
|
||||
watch_cb_t tick_callbacks[8];
|
||||
comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB];
|
||||
|
||||
static uint32_t scheduled_comp_counter;
|
||||
|
||||
static long alarm_interval_id = -1;
|
||||
static long alarm_timeout_id = -1;
|
||||
@@ -40,41 +63,73 @@ watch_cb_t btn_alarm_callback;
|
||||
watch_cb_t a2_callback;
|
||||
watch_cb_t a4_callback;
|
||||
|
||||
static void _watch_increase_counter(void *userData);
|
||||
static void _watch_process_periodic_callbacks(void);
|
||||
static void _watch_process_comp_callbacks(void);
|
||||
|
||||
bool _watch_rtc_is_enabled(void) {
|
||||
return true;
|
||||
return rtc_enabled;
|
||||
}
|
||||
|
||||
void _watch_rtc_init(void) {
|
||||
#if EMSCRIPTEN
|
||||
// Shifts the timezone so our local time is converted to UTC and set
|
||||
for (uint8_t index = 0; index < 8; ++index) {
|
||||
tick_callbacks[index] = NULL;
|
||||
}
|
||||
|
||||
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
|
||||
comp_callbacks[index].counter = 0;
|
||||
comp_callbacks[index].callback = NULL;
|
||||
comp_callbacks[index].enabled = false;
|
||||
}
|
||||
|
||||
scheduled_comp_counter = 0;
|
||||
counter = 0;
|
||||
rtc_enabled = false;
|
||||
|
||||
watch_rtc_set_date_time(watch_get_init_date_time());
|
||||
watch_rtc_enable(true);
|
||||
}
|
||||
|
||||
void watch_rtc_set_date_time(rtc_date_time_t date_time) {
|
||||
watch_rtc_set_unix_time(watch_utility_date_time_to_unix_time(date_time, 0));
|
||||
}
|
||||
|
||||
rtc_date_time_t watch_rtc_get_date_time(void) {
|
||||
return watch_utility_date_time_from_unix_time(watch_rtc_get_unix_time(), 0);
|
||||
}
|
||||
|
||||
void watch_rtc_set_unix_time(unix_timestamp_t unix_time) {
|
||||
// unix_time = time_backup + counter / RTC_CNT_HZ - 0.5
|
||||
rtc_counter_t counter = watch_rtc_get_counter();
|
||||
reference_timestamp = unix_time - (counter >> RTC_CNT_DIV) - ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) + 1;
|
||||
}
|
||||
|
||||
unix_timestamp_t watch_rtc_get_unix_time(void) {
|
||||
// unix_time = time_backup + counter / RTC_CNT_HZ - 0.5
|
||||
rtc_counter_t counter = watch_rtc_get_counter();
|
||||
return reference_timestamp + (counter >> RTC_CNT_DIV) + ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) - 1;
|
||||
}
|
||||
|
||||
rtc_counter_t watch_rtc_get_counter(void) {
|
||||
return counter;
|
||||
}
|
||||
|
||||
uint32_t watch_rtc_get_frequency(void) {
|
||||
return RTC_CNT_HZ;
|
||||
}
|
||||
|
||||
uint32_t watch_rtc_get_ticks_per_minute(void) {
|
||||
return RTC_CNT_TICKS_PER_MINUTE;
|
||||
}
|
||||
|
||||
rtc_date_time_t watch_get_init_date_time(void) {
|
||||
rtc_date_time_t date_time = {0};
|
||||
|
||||
int32_t time_zone_offset = EM_ASM_INT({
|
||||
return -new Date().getTimezoneOffset() * 60;
|
||||
return new Date().getTimezoneOffset() * 60 * 1000; // ms
|
||||
});
|
||||
#endif
|
||||
#ifdef BUILD_YEAR
|
||||
watch_date_time_t date_time = watch_get_init_date_time();
|
||||
#else
|
||||
watch_date_time_t date_time = watch_rtc_get_date_time();
|
||||
#endif
|
||||
watch_rtc_set_date_time(watch_utility_date_time_convert_zone(date_time, time_zone_offset, 0));
|
||||
}
|
||||
|
||||
void watch_rtc_set_date_time(watch_date_time_t date_time) {
|
||||
time_offset = EM_ASM_DOUBLE({
|
||||
const year = 2020 + (($0 >> 26) & 0x3f);
|
||||
const month = ($0 >> 22) & 0xf;
|
||||
const day = ($0 >> 17) & 0x1f;
|
||||
const hour = ($0 >> 12) & 0x1f;
|
||||
const minute = ($0 >> 6) & 0x3f;
|
||||
const second = $0 & 0x3f;
|
||||
const date = new Date(year, month - 1, day, hour, minute, second);
|
||||
return date - Date.now();
|
||||
}, date_time.reg);
|
||||
}
|
||||
|
||||
watch_date_time_t watch_rtc_get_date_time(void) {
|
||||
watch_date_time_t retval;
|
||||
retval.reg = EM_ASM_INT({
|
||||
date_time.reg = EM_ASM_INT({
|
||||
const date = new Date(Date.now() + $0);
|
||||
return date.getSeconds() |
|
||||
(date.getMinutes() << 6) |
|
||||
@@ -82,27 +137,16 @@ watch_date_time_t watch_rtc_get_date_time(void) {
|
||||
(date.getDate() << 17) |
|
||||
((date.getMonth() + 1) << 22) |
|
||||
((date.getFullYear() - 2020) << 26);
|
||||
}, time_offset);
|
||||
return retval;
|
||||
}
|
||||
|
||||
rtc_date_time_t watch_get_init_date_time(void) {
|
||||
rtc_date_time_t date_time = {0};
|
||||
}, time_zone_offset);
|
||||
|
||||
#ifdef BUILD_YEAR
|
||||
date_time.unit.year = BUILD_YEAR;
|
||||
#else
|
||||
date_time.unit.year = 5;
|
||||
#endif
|
||||
#ifdef BUILD_MONTH
|
||||
date_time.unit.month = BUILD_MONTH;
|
||||
#else
|
||||
date_time.unit.month = 1;
|
||||
#endif
|
||||
#ifdef BUILD_DAY
|
||||
date_time.unit.day = BUILD_DAY;
|
||||
#else
|
||||
date_time.unit.day = 1;
|
||||
#endif
|
||||
#ifdef BUILD_HOUR
|
||||
date_time.unit.hour = BUILD_HOUR;
|
||||
@@ -122,10 +166,94 @@ void watch_rtc_disable_tick_callback(void) {
|
||||
watch_rtc_disable_periodic_callback(1);
|
||||
}
|
||||
|
||||
static void watch_invoke_periodic_callback(void *userData) {
|
||||
watch_cb_t callback = userData;
|
||||
callback();
|
||||
static void _watch_schedule_next_tick(void) {
|
||||
if (!rtc_enabled) return;
|
||||
|
||||
double now = EM_ASM_DOUBLE({ return performance.now(); });
|
||||
|
||||
// Target interval in ms
|
||||
double ms = 1000.0 / (double)RTC_CNT_HZ;
|
||||
|
||||
// Schedule next tick, correcting for drift
|
||||
next_tick_time += ms;
|
||||
double delay = next_tick_time - now;
|
||||
|
||||
// If we're behind, reset timing
|
||||
if (delay < 0) {
|
||||
next_tick_time = now + ms;
|
||||
delay = ms;
|
||||
}
|
||||
|
||||
emscripten_async_call(_watch_increase_counter, NULL, delay);
|
||||
}
|
||||
|
||||
static void _watch_increase_counter(void *userData) {
|
||||
(void) userData;
|
||||
|
||||
counter += 1;
|
||||
// Fire the periodic callbacks that match this counter
|
||||
_watch_process_periodic_callbacks();
|
||||
// Fire the comp callbacks that match this counter
|
||||
_watch_process_comp_callbacks();
|
||||
|
||||
resume_main_loop();
|
||||
|
||||
// Schedule the next tick with drift correction
|
||||
_watch_schedule_next_tick();
|
||||
}
|
||||
|
||||
static void _watch_process_periodic_callbacks(void) {
|
||||
/* It looks weird but it follows the way the hardware triggers periodic interrupts.
|
||||
* For 128hz counter periodic interrupts fire at these tick values:
|
||||
* 1Hz: 64
|
||||
* 2Hz: 32, 96
|
||||
* 4Hz: 16, 48, 80, 112
|
||||
* 8Hz: 8, 24, 40, 56, 72, 88, 104, 120
|
||||
* 16Hz: 4, 12, 20, ..., 124
|
||||
* 32Hz: 2, 6, 10, ..., 126
|
||||
* 64Hz: 1, 3, 5, ..., 127
|
||||
* 128Hz: 0, 1, 2, ..., 127
|
||||
*
|
||||
* Which means that only one periodic interrupt can fire for a given counter value
|
||||
* (except 128Hz which can always fire)
|
||||
*/
|
||||
|
||||
uint32_t freq = watch_rtc_get_frequency();
|
||||
uint32_t subsecond_mask = freq - 1;
|
||||
uint32_t subseconds = counter & subsecond_mask;
|
||||
|
||||
// Find the firs non-zero bit in the counter, which can be used to determine the appropriate period (see table above).
|
||||
uint8_t per_n = 0;
|
||||
|
||||
for (uint8_t i = 0; i < 7; i++) {
|
||||
if (subseconds & (1 << i)) {
|
||||
per_n = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tick_callbacks[per_n]) {
|
||||
tick_callbacks[per_n]();
|
||||
}
|
||||
|
||||
// 128Hz is always a match
|
||||
if (per_n != 0 && tick_callbacks[0]) {
|
||||
tick_callbacks[0]();
|
||||
}
|
||||
}
|
||||
|
||||
static void _watch_process_comp_callbacks(void) {
|
||||
// In hardware the interrupt fires one tick after the matching counter
|
||||
if (counter == (scheduled_comp_counter + 1)) {
|
||||
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
|
||||
if (comp_callbacks[index].enabled && scheduled_comp_counter == comp_callbacks[index].counter) {
|
||||
comp_callbacks[index].enabled = false;
|
||||
comp_callbacks[index].callback();
|
||||
}
|
||||
}
|
||||
|
||||
watch_rtc_schedule_next_comp();
|
||||
}
|
||||
}
|
||||
|
||||
void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency) {
|
||||
@@ -138,26 +266,19 @@ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency
|
||||
// 0x01 (1 Hz) will have 7 leading zeros for PER7. 0xF0 (128 Hz) will have no leading zeroes for PER0.
|
||||
uint8_t per_n = __builtin_clz(tmp);
|
||||
|
||||
double interval = 1000.0 / frequency; // in msec
|
||||
|
||||
if (tick_callbacks[per_n] != -1) emscripten_clear_interval(tick_callbacks[per_n]);
|
||||
tick_callbacks[per_n] = emscripten_set_interval(watch_invoke_periodic_callback, interval, (void *)callback);
|
||||
tick_callbacks[per_n] = callback;
|
||||
}
|
||||
|
||||
void watch_rtc_disable_periodic_callback(uint8_t frequency) {
|
||||
if (__builtin_popcount(frequency) != 1) return;
|
||||
uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24);
|
||||
if (tick_callbacks[per_n] != -1) {
|
||||
emscripten_clear_interval(tick_callbacks[per_n]);
|
||||
tick_callbacks[per_n] = -1;
|
||||
}
|
||||
tick_callbacks[per_n] = NULL;
|
||||
}
|
||||
|
||||
void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (tick_callbacks[i] != -1 && (mask & (1 << i)) != 0) {
|
||||
emscripten_clear_interval(tick_callbacks[i]);
|
||||
tick_callbacks[i] = -1;
|
||||
if (tick_callbacks[i] && (mask & (1 << i)) != 0) {
|
||||
tick_callbacks[i] = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,81 +287,95 @@ void watch_rtc_disable_all_periodic_callbacks(void) {
|
||||
watch_rtc_disable_matching_periodic_callbacks(0xFF);
|
||||
}
|
||||
|
||||
static void watch_invoke_alarm_interval_callback(void *userData) {
|
||||
if (alarm_callback) alarm_callback();
|
||||
}
|
||||
|
||||
static void watch_invoke_alarm_callback(void *userData) {
|
||||
if (alarm_callback) alarm_callback();
|
||||
alarm_interval_id = emscripten_set_interval(watch_invoke_alarm_interval_callback, alarm_interval, NULL);
|
||||
}
|
||||
|
||||
void watch_rtc_register_alarm_callback(watch_cb_t callback, watch_date_time_t alarm_time, rtc_alarm_match_t mask) {
|
||||
watch_rtc_disable_alarm_callback();
|
||||
|
||||
switch (mask) {
|
||||
case ALARM_MATCH_DISABLED:
|
||||
void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) {
|
||||
if (index >= WATCH_RTC_N_COMP_CB) {
|
||||
return;
|
||||
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({
|
||||
const now = Date.now();
|
||||
const date = new Date(now);
|
||||
comp_callbacks[index].counter = counter;
|
||||
comp_callbacks[index].callback = callback;
|
||||
comp_callbacks[index].enabled = true;
|
||||
|
||||
const hour = ($0 >> 12) & 0x1f;
|
||||
const minute = ($0 >> 6) & 0x3f;
|
||||
const second = $0 & 0x3f;
|
||||
|
||||
if ($1 == 1) { // SS
|
||||
if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1);
|
||||
date.setSeconds(second);
|
||||
} else if ($1 == 2) { // MMSS
|
||||
if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1);
|
||||
if (minute < date.getMinutes()) date.setHours(date.getHours() + 1);
|
||||
date.setMinutes(minute, second);
|
||||
} else if ($1 == 3) { // HHMMSS
|
||||
if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1);
|
||||
if (minute < date.getMinutes()) date.setHours(date.getHours() + 1);
|
||||
if (hour < date.getHours()) date.setDate(date.getDate() + 1);
|
||||
date.setHours(hour, minute, second);
|
||||
} else {
|
||||
throw 'Invalid alarm match mask';
|
||||
}
|
||||
|
||||
return date - now;
|
||||
}, alarm_time.reg, mask);
|
||||
|
||||
alarm_callback = callback;
|
||||
alarm_timeout_id = emscripten_set_timeout(watch_invoke_alarm_callback, timeout, NULL);
|
||||
watch_rtc_schedule_next_comp();
|
||||
}
|
||||
|
||||
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;
|
||||
void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) {
|
||||
if (index >= WATCH_RTC_N_COMP_CB) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (alarm_interval_id != -1) {
|
||||
emscripten_clear_interval(alarm_interval_id);
|
||||
alarm_interval_id = -1;
|
||||
comp_callbacks[index].counter = counter;
|
||||
comp_callbacks[index].callback = callback;
|
||||
comp_callbacks[index].enabled = true;
|
||||
}
|
||||
|
||||
void watch_rtc_disable_comp_callback(uint8_t index) {
|
||||
if (index >= WATCH_RTC_N_COMP_CB) {
|
||||
return;
|
||||
}
|
||||
|
||||
comp_callbacks[index].enabled = false;
|
||||
|
||||
watch_rtc_schedule_next_comp();
|
||||
}
|
||||
|
||||
void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) {
|
||||
if (index >= WATCH_RTC_N_COMP_CB) {
|
||||
return;
|
||||
}
|
||||
|
||||
comp_callbacks[index].enabled = false;
|
||||
}
|
||||
|
||||
void watch_rtc_schedule_next_comp(void) {
|
||||
rtc_counter_t curr_counter = watch_rtc_get_counter();
|
||||
// If there is already a pending comp interrupt for this very tick, let it fire
|
||||
// And this function will be called again as soon as the interrupt fires.
|
||||
if (curr_counter == scheduled_comp_counter) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The soonest we can schedule is the next tick
|
||||
curr_counter +=1;
|
||||
|
||||
bool schedule_any = false;
|
||||
rtc_counter_t comp_counter;
|
||||
rtc_counter_t min_diff = UINT_MAX;
|
||||
|
||||
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
|
||||
// rtc_counter_t diff =
|
||||
if (comp_callbacks[index].enabled) {
|
||||
rtc_counter_t diff = comp_callbacks[index].counter - curr_counter;
|
||||
if (diff <= min_diff) {
|
||||
min_diff = diff;
|
||||
comp_counter = comp_callbacks[index].counter;
|
||||
schedule_any = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schedule_any) {
|
||||
scheduled_comp_counter = comp_counter;
|
||||
} else {
|
||||
scheduled_comp_counter = curr_counter - 2;
|
||||
}
|
||||
}
|
||||
|
||||
void watch_rtc_enable(bool en)
|
||||
{
|
||||
//Not simulated
|
||||
// Nothing to do cases
|
||||
if ((en && rtc_enabled) || (!en && !rtc_enabled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (en) {
|
||||
// Use drift-correcting timer instead of fixed setInterval
|
||||
rtc_enabled = true;
|
||||
next_tick_time = EM_ASM_DOUBLE({ return performance.now(); });
|
||||
_watch_schedule_next_tick();
|
||||
} else {
|
||||
rtc_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
void watch_rtc_freqcorr_write(int16_t value, int16_t sign)
|
||||
|
||||
@@ -50,17 +50,23 @@ void watch_enable_display(void) {
|
||||
_watch_update_indicator_segments();
|
||||
#endif
|
||||
|
||||
EM_ASM({
|
||||
#if defined(FORCE_CUSTOM_LCD_TYPE)
|
||||
document.getElementById("classic").style.display = "none";
|
||||
EM_ASM({document.getElementById("custom").style.display = "";});
|
||||
EM_ASM({document.getElementById("classic").style.display = "none";});
|
||||
#else
|
||||
document.getElementById("custom").style.display = "none";
|
||||
EM_ASM({document.getElementById("custom").style.display = "none";});
|
||||
EM_ASM({document.getElementById("classic").style.display = "";});
|
||||
#endif
|
||||
});
|
||||
|
||||
watch_clear_display();
|
||||
}
|
||||
|
||||
void watch_disable_display(void) {
|
||||
watch_clear_display();
|
||||
EM_ASM({document.getElementById("classic").style.display = "none";});
|
||||
EM_ASM({document.getElementById("custom").style.display = "none";});
|
||||
}
|
||||
|
||||
void watch_set_pixel(uint8_t com, uint8_t seg) {
|
||||
EM_ASM({
|
||||
document.querySelectorAll("[data-com='" + $0 + "'][data-seg='" + $1 + "']")
|
||||
|
||||
@@ -28,18 +28,23 @@
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
|
||||
static bool buzzer_enabled = false;
|
||||
static volatile bool buzzer_enabled = false;
|
||||
static uint32_t buzzer_period;
|
||||
|
||||
void cb_watch_buzzer_seq(void *userData);
|
||||
void cb_watch_buzzer_raw_source(void *userData);
|
||||
|
||||
static uint16_t _seq_position;
|
||||
static int8_t _tone_ticks, _repeat_counter;
|
||||
static long _em_interval_id = 0;
|
||||
static volatile long _em_interval_id = 0;
|
||||
static int8_t *_sequence;
|
||||
static watch_buzzer_raw_source_t _raw_source;
|
||||
static void* _userdata;
|
||||
static uint8_t _volume;
|
||||
static void (*_cb_finished)(void);
|
||||
|
||||
void _watch_enable_tcc(void) {}
|
||||
static watch_cb_t _cb_start_global = NULL;
|
||||
static watch_cb_t _cb_stop_global = NULL;
|
||||
static volatile bool _buzzer_is_active = false;
|
||||
|
||||
static inline void _em_interval_stop() {
|
||||
emscripten_clear_interval(_em_interval_id);
|
||||
@@ -47,15 +52,28 @@ static inline void _em_interval_stop() {
|
||||
}
|
||||
|
||||
void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) {
|
||||
if (_em_interval_id) _em_interval_stop();
|
||||
watch_buzzer_play_sequence_with_volume(note_sequence, callback_on_end, WATCH_BUZZER_VOLUME_LOUD);
|
||||
}
|
||||
|
||||
void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) {
|
||||
watch_buzzer_abort_sequence();
|
||||
|
||||
// prepare buzzer
|
||||
watch_enable_buzzer();
|
||||
watch_set_buzzer_off();
|
||||
|
||||
_buzzer_is_active = true;
|
||||
|
||||
if (_cb_start_global) {
|
||||
_cb_start_global();
|
||||
}
|
||||
|
||||
_sequence = note_sequence;
|
||||
_cb_finished = callback_on_end;
|
||||
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
|
||||
_seq_position = 0;
|
||||
_tone_ticks = 0;
|
||||
_repeat_counter = -1;
|
||||
// prepare buzzer
|
||||
watch_enable_buzzer();
|
||||
// initiate 64 hz callback
|
||||
_em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL);
|
||||
}
|
||||
@@ -88,32 +106,115 @@ void cb_watch_buzzer_seq(void *userData) {
|
||||
if (note == BUZZER_NOTE_REST) {
|
||||
watch_set_buzzer_off();
|
||||
} else {
|
||||
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], 25);
|
||||
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], _volume);
|
||||
watch_set_buzzer_on();
|
||||
}
|
||||
// set duration ticks and move to next tone
|
||||
_tone_ticks = _sequence[_seq_position + 1];
|
||||
_tone_ticks = _sequence[_seq_position + 1] - 1;
|
||||
_seq_position += 2;
|
||||
} else {
|
||||
// end the sequence
|
||||
watch_buzzer_abort_sequence();
|
||||
if (_cb_finished) _cb_finished();
|
||||
}
|
||||
} else _tone_ticks--;
|
||||
}
|
||||
|
||||
void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end) {
|
||||
watch_buzzer_play_raw_source_with_volume(raw_source, userdata, callback_on_end, WATCH_BUZZER_VOLUME_LOUD);
|
||||
}
|
||||
|
||||
void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume) {
|
||||
watch_buzzer_abort_sequence();
|
||||
|
||||
// prepare buzzer
|
||||
watch_enable_buzzer();
|
||||
watch_set_buzzer_off();
|
||||
|
||||
_buzzer_is_active = true;
|
||||
|
||||
if (_cb_start_global) {
|
||||
_cb_start_global();
|
||||
}
|
||||
|
||||
_raw_source = raw_source;
|
||||
_userdata = userdata;
|
||||
_cb_finished = callback_on_end;
|
||||
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
|
||||
_seq_position = 0;
|
||||
_tone_ticks = 0;
|
||||
|
||||
// initiate 64 hz callback
|
||||
_em_interval_id = emscripten_set_interval(cb_watch_buzzer_raw_source, (double)(1000/64), (void *)NULL);
|
||||
}
|
||||
|
||||
void cb_watch_buzzer_raw_source(void *userData) {
|
||||
// callback for reading the note sequence
|
||||
(void) userData;
|
||||
uint16_t period;
|
||||
uint16_t duration;
|
||||
bool done;
|
||||
|
||||
if (_tone_ticks == 0) {
|
||||
done = _raw_source(_seq_position, _userdata, &period, &duration);
|
||||
|
||||
if (done || duration == 0) {
|
||||
// end the sequence
|
||||
watch_buzzer_abort_sequence();
|
||||
} else {
|
||||
if (period == WATCH_BUZZER_PERIOD_REST) {
|
||||
watch_set_buzzer_off();
|
||||
} else {
|
||||
watch_set_buzzer_period_and_duty_cycle(period, _volume);
|
||||
watch_set_buzzer_on();
|
||||
}
|
||||
|
||||
// set duration ticks and move to next tone
|
||||
_tone_ticks = duration - 1;
|
||||
_seq_position += 1;
|
||||
}
|
||||
} else {
|
||||
_tone_ticks--;
|
||||
}
|
||||
}
|
||||
|
||||
void watch_buzzer_abort_sequence(void) {
|
||||
// ends/aborts the sequence
|
||||
if (_em_interval_id) _em_interval_stop();
|
||||
|
||||
watch_set_buzzer_off();
|
||||
watch_disable_buzzer();
|
||||
|
||||
if (!_buzzer_is_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
_buzzer_is_active = false;
|
||||
|
||||
if (_cb_stop_global) {
|
||||
_cb_stop_global();
|
||||
}
|
||||
|
||||
if (_cb_finished) {
|
||||
_cb_finished();
|
||||
}
|
||||
}
|
||||
|
||||
void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop) {
|
||||
_cb_stop_global = cb_start;
|
||||
_cb_stop_global = cb_stop;
|
||||
}
|
||||
|
||||
void watch_enable_buzzer(void) {
|
||||
watch_buzzer_abort_sequence();
|
||||
buzzer_enabled = true;
|
||||
buzzer_period = NotePeriods[BUZZER_NOTE_A4];
|
||||
|
||||
EM_ASM({
|
||||
// "It's recommended to create one AudioContext and reuse it instead of initializing a new one each time."
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/AudioContext
|
||||
if (!Module['audioContext']) {
|
||||
Module['audioContext'] = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,13 +227,6 @@ void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty_cycle)
|
||||
void watch_disable_buzzer(void) {
|
||||
buzzer_enabled = false;
|
||||
buzzer_period = NotePeriods[BUZZER_NOTE_A4];
|
||||
|
||||
EM_ASM({
|
||||
if (Module['audioContext']) {
|
||||
Module['audioContext'].close();
|
||||
Module['audioContext'] = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void watch_set_buzzer_on(void) {
|
||||
@@ -175,52 +269,46 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms) {
|
||||
}
|
||||
|
||||
void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) {
|
||||
if (note == BUZZER_NOTE_REST) {
|
||||
watch_set_buzzer_off();
|
||||
} else {
|
||||
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25);
|
||||
watch_set_buzzer_on();
|
||||
}
|
||||
static int8_t single_note_sequence[3];
|
||||
|
||||
main_loop_sleep(duration_ms);
|
||||
watch_set_buzzer_off();
|
||||
single_note_sequence[0] = note;
|
||||
// 64 ticks per second for the tc0
|
||||
// Each tick is approximately 15ms
|
||||
uint16_t duration = duration_ms / 15;
|
||||
if (duration > 127) duration = 127;
|
||||
single_note_sequence[1] = (int8_t)duration;
|
||||
single_note_sequence[2] = 0;
|
||||
|
||||
watch_buzzer_play_sequence_with_volume(single_note_sequence, NULL, volume);
|
||||
}
|
||||
|
||||
void watch_enable_leds(void) {}
|
||||
|
||||
void watch_disable_leds(void) {}
|
||||
|
||||
void watch_set_led_color(uint8_t red, uint8_t green) {
|
||||
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) {
|
||||
EM_ASM({
|
||||
// the watch svg contains an feColorMatrix filter with id ledcolor
|
||||
// and a green svg gradient that mimics the led being on
|
||||
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feColorMatrix
|
||||
// this changes the color of the gradient to match the red+green combination
|
||||
let filter = document.getElementById("ledcolor");
|
||||
let color_matrix = filter.children[0].values.baseVal;
|
||||
color_matrix[1].value = $0 / 255; // red value
|
||||
color_matrix[6].value = $1 / 255; // green value
|
||||
document.getElementById('light').style.opacity = Math.min(255, $0 + $1) / 255;
|
||||
}, red, green);
|
||||
}
|
||||
|
||||
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) {
|
||||
(void) blue;
|
||||
watch_set_led_color(red, green);
|
||||
color_matrix[0].value = $0 / 255; // red
|
||||
color_matrix[6].value = $1 / 255; // green
|
||||
color_matrix[12].value = $2 / 255; // blue
|
||||
document.getElementById('light').style.opacity = Math.min(255, $0 + $1 + $2) / 255;
|
||||
}, red, green, blue);
|
||||
}
|
||||
|
||||
void watch_set_led_red(void) {
|
||||
watch_set_led_color(255, 0);
|
||||
watch_set_led_color_rgb(255, 0, 0);
|
||||
}
|
||||
|
||||
void watch_set_led_green(void) {
|
||||
watch_set_led_color(0, 255);
|
||||
watch_set_led_color_rgb(0, 255, 0);
|
||||
}
|
||||
|
||||
void watch_set_led_yellow(void) {
|
||||
watch_set_led_color(255, 255);
|
||||
watch_set_led_color_rgb(255, 255, 0);
|
||||
}
|
||||
|
||||
void watch_set_led_off(void) {
|
||||
watch_set_led_color(0, 0);
|
||||
watch_set_led_color_rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user