1 Commits

Author SHA1 Message Date
hueso 78bdbb0e15 add moonrise/moonset face
Build / build (sensorwatch_blue, classic) (push) Has been cancelled
Build / build (sensorwatch_blue, custom) (push) Has been cancelled
Build / build (sensorwatch_green, classic) (push) Has been cancelled
Build / build (sensorwatch_pro, classic) (push) Has been cancelled
Build / build (sensorwatch_pro, custom) (push) Has been cancelled
Build / build (sensorwatch_red, custom) (push) Has been cancelled
Build / build-simulator (sensorwatch_blue, classic) (push) Has been cancelled
Build / build-simulator (sensorwatch_blue, custom) (push) Has been cancelled
Build / build-simulator (sensorwatch_green, classic) (push) Has been cancelled
Build / build-simulator (sensorwatch_green, custom) (push) Has been cancelled
Build / build-simulator (sensorwatch_pro, classic) (push) Has been cancelled
Build / build-simulator (sensorwatch_pro, custom) (push) Has been cancelled
Build / build-simulator (sensorwatch_red, classic) (push) Has been cancelled
Build / build-simulator (sensorwatch_red, custom) (push) Has been cancelled
Build / build (sensorwatch_green, custom) (push) Has been cancelled
Build / build (sensorwatch_red, classic) (push) Has been cancelled
2025-12-16 14:09:52 -03:00
62 changed files with 1120 additions and 4589 deletions
-4
View File
@@ -20,9 +20,6 @@ TINYUSB_CDC=1
# Now we're all set to include gossamer's make rules. # Now we're all set to include gossamer's make rules.
include $(GOSSAMER_PATH)/make.mk include $(GOSSAMER_PATH)/make.mk
# Don't add gossamer's rtc.c since we are using our own rtc32.c
SRCS := $(filter-out $(GOSSAMER_PATH)/peripherals/rtc.c,$(SRCS))
CFLAGS+=-D_POSIX_C_SOURCE=200112L CFLAGS+=-D_POSIX_C_SOURCE=200112L
define n define n
@@ -141,7 +138,6 @@ INCLUDES += \
-I./watch-library/hardware/watch \ -I./watch-library/hardware/watch \
SRCS += \ SRCS += \
./watch-library/hardware/watch/rtc32.c \
./watch-library/hardware/watch/watch.c \ ./watch-library/hardware/watch/watch.c \
./watch-library/hardware/watch/watch_adc.c \ ./watch-library/hardware/watch/watch_adc.c \
./watch-library/hardware/watch/watch_deepsleep.c \ ./watch-library/hardware/watch/watch_deepsleep.c \
+1 -1
View File
@@ -1,7 +1,7 @@
Second Movement Second Movement
=============== ===============
This is the successor refactor of the Movement firmware for [Sensor Watch](https://www.sensorwatch.net). This is a work-in-progress refactor of the Movement firmware for [Sensor Watch](https://www.sensorwatch.net).
Getting dependencies Getting dependencies
@@ -25,6 +25,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "wyoscan_face.h" #include "wyoscan_face.h"
#include "watch_private_display.h"
/* /*
Slowly render the current time from left to right, Slowly render the current time from left to right,
@@ -114,7 +115,7 @@ bool wyoscan_face_loop(movement_event_t event, void *context) {
break; break;
case EVENT_TICK: case EVENT_TICK:
if (!state->animate) { if (!state->animate) {
date_time = movement_get_local_date_time(); date_time = watch_rtc_get_date_time();
state->start = 0; state->start = 0;
state->end = 0; state->end = 0;
state->animation = 0; state->animation = 0;
@@ -448,13 +448,13 @@ static void start_reading(accelerometer_data_acquisition_state_t *state) {
state->records[state->pos++] = record; state->records[state->pos++] = record;
lis2dw_fifo_t fifo; lis2dw_fifo_t fifo;
lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT); // dump the fifo, this starts a fresh round of data in continue_reading lis2dw_read_fifo(&fifo); // dump the fifo, this starts a fresh round of data in continue_reading
} }
static void continue_reading(accelerometer_data_acquisition_state_t *state) { static void continue_reading(accelerometer_data_acquisition_state_t *state) {
printf("Continue reading\n"); printf("Continue reading\n");
lis2dw_fifo_t fifo; lis2dw_fifo_t fifo;
lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT); lis2dw_read_fifo(&fifo);
fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it. fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it.
uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second. uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second.
+260 -727
View File
File diff suppressed because it is too large Load Diff
+23 -43
View File
@@ -2,7 +2,6 @@
* MIT License * MIT License
* *
* Copyright (c) 2022 Joey Castillo * Copyright (c) 2022 Joey Castillo
* Copyright (c) 2025 Alessandro Genova
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -121,43 +120,20 @@ typedef enum {
EVENT_LIGHT_BUTTON_UP, // The light button was pressed for less than half a second, and released. EVENT_LIGHT_BUTTON_UP, // The light button was pressed for less than half a second, and released.
EVENT_LIGHT_LONG_PRESS, // The light button was held for over half a second, but not yet released. EVENT_LIGHT_LONG_PRESS, // The light button was held for over half a second, but not yet released.
EVENT_LIGHT_LONG_UP, // The light button was held for over half a second, and released. EVENT_LIGHT_LONG_UP, // The light button was held for over half a second, and released.
EVENT_LIGHT_REALLY_LONG_PRESS, // The light button was held for more than 1.5 second, note yet released.
// EVENT_LIGHT_REALLY_LONG_UP, // The light button was held for more than 1.5 second, and released.
EVENT_MODE_BUTTON_DOWN, // The mode button has been pressed, but not yet released. EVENT_MODE_BUTTON_DOWN, // The mode button has been pressed, but not yet released.
EVENT_MODE_BUTTON_UP, // The mode button was pressed for less than half a second, and released. EVENT_MODE_BUTTON_UP, // The mode button was pressed for less than half a second, and released.
EVENT_MODE_LONG_PRESS, // The mode button was held for over half a second, but not yet released. EVENT_MODE_LONG_PRESS, // The mode button was held for over half a second, but not yet released.
EVENT_MODE_LONG_UP, // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event. EVENT_MODE_LONG_UP, // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event.
EVENT_MODE_REALLY_LONG_PRESS, // The mode button was held for more than 1.5 second, note yet released.
// EVENT_MODE_REALLY_LONG_UP, // The mode button was held for more than 1.5 second, and released.
EVENT_ALARM_BUTTON_DOWN, // The alarm button has been pressed, but not yet released. EVENT_ALARM_BUTTON_DOWN, // The alarm button has been pressed, but not yet released.
EVENT_ALARM_BUTTON_UP, // The alarm button was pressed for less than half a second, and released. EVENT_ALARM_BUTTON_UP, // The alarm button was pressed for less than half a second, and released.
EVENT_ALARM_LONG_PRESS, // The alarm button was held for over half a second, but not yet released. EVENT_ALARM_LONG_PRESS, // The alarm button was held for over half a second, but not yet released.
EVENT_ALARM_LONG_UP, // The alarm button was held for over half a second, and released. EVENT_ALARM_LONG_UP, // The alarm button was held for over half a second, and released.
EVENT_ALARM_REALLY_LONG_PRESS, // The alarm button was held for more than 1.5 second, note yet released.
// EVENT_ALARM_REALLY_LONG_UP, // The alarm button was held for more than 1.5 second, and released.
EVENT_ACCELEROMETER_WAKE, // The accelerometer has detected motion and woken up. EVENT_ACCELEROMETER_WAKE, // The accelerometer has detected motion and woken up.
EVENT_SINGLE_TAP, // Accelerometer detected a single tap. This event is not yet implemented. EVENT_SINGLE_TAP, // Accelerometer detected a single tap. This event is not yet implemented.
EVENT_DOUBLE_TAP, // Accelerometer detected a double tap. This event is not yet implemented. EVENT_DOUBLE_TAP, // Accelerometer detected a double tap. This event is not yet implemented.
} movement_event_type_t; } movement_event_type_t;
// Each different timeout type will use a different index when invoking watch_rtc_register_comp_callback
typedef enum {
LIGHT_BUTTON_TIMEOUT = 0, // Light button longpress timeout
MODE_BUTTON_TIMEOUT, // Mode button longpress timeout
ALARM_BUTTON_TIMEOUT, // Alarm button longpress timeout
LED_TIMEOUT, // LED off timeout
RESIGN_TIMEOUT, // Resign active face timeout
SLEEP_TIMEOUT, // Low-energy begin timeout
MINUTE_TIMEOUT, // Top of the Minute timeout
} movement_timeout_index_t;
typedef enum {
BUZZER_PRIORITY_BUTTON = 0, // Buzzer priority for button beeps (lowest priority).
BUZZER_PRIORITY_SIGNAL, // Buzzer priority for hourly chime (medium priority).
BUZZER_PRIORITY_ALARM, // Buzzer priority for hourly chime (highest priority).
} movement_buzzer_priority_t;
typedef struct { typedef struct {
uint8_t event_type; uint8_t event_type;
uint8_t subsecond; uint8_t subsecond;
@@ -273,16 +249,37 @@ typedef struct {
int16_t current_face_idx; int16_t current_face_idx;
int16_t next_face_idx; int16_t next_face_idx;
bool watch_face_changed; bool watch_face_changed;
bool fast_tick_enabled;
int16_t fast_ticks;
// LED stuff // LED stuff
bool light_on; 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;
// background task handling // background task handling
bool woke_from_alarm_handler;
bool has_scheduled_background_task; bool has_scheduled_background_task;
bool needs_wake;
// low energy mode countdown
int32_t le_mode_ticks;
// app resignation countdown (TODO: consolidate with LE countdown?)
int16_t timeout_ticks;
// stuff for subsecond tracking // stuff for subsecond tracking
uint8_t tick_frequency; uint8_t tick_frequency;
uint8_t tick_pern; uint8_t last_second;
uint8_t subsecond;
// backup register stuff // backup register stuff
uint8_t next_available_backup_register; uint8_t next_available_backup_register;
@@ -299,10 +296,6 @@ typedef struct {
lis2dw_data_rate_t accelerometer_background_rate; lis2dw_data_rate_t accelerometer_background_rate;
// threshold for considering the wearer is in motion // threshold for considering the wearer is in motion
uint8_t accelerometer_motion_threshold; uint8_t accelerometer_motion_threshold;
// signal and alarm volumes
watch_buzzer_volume_t signal_volume;
watch_buzzer_volume_t alarm_volume;
} movement_state_t; } movement_state_t;
void movement_move_to_face(uint8_t watch_face_index); void movement_move_to_face(uint8_t watch_face_index);
@@ -331,18 +324,14 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index);
void movement_request_sleep(void); void movement_request_sleep(void);
void movement_request_wake(void); void movement_request_wake(void);
void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms);
void movement_play_signal(void); void movement_play_signal(void);
void movement_play_alarm(void); void movement_play_alarm(void);
void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note); void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note);
void movement_play_sequence(int8_t *note_sequence, movement_buzzer_priority_t priority);
uint8_t movement_claim_backup_register(void); uint8_t movement_claim_backup_register(void);
int32_t movement_get_current_timezone_offset_for_zone(uint8_t zone_index); int32_t movement_get_current_timezone_offset_for_zone(uint8_t zone_index);
int32_t movement_get_current_timezone_offset(void); int32_t movement_get_current_timezone_offset(void);
int32_t movement_get_timezone_offset_for_date_in_zone(watch_date_time_t date_time, uint8_t zone_index);
int32_t movement_get_timezone_offset_for_date(watch_date_time_t date_time);
int32_t movement_get_timezone_index(void); int32_t movement_get_timezone_index(void);
void movement_set_timezone_index(uint8_t value); void movement_set_timezone_index(uint8_t value);
@@ -350,11 +339,8 @@ void movement_set_timezone_index(uint8_t value);
watch_date_time_t movement_get_utc_date_time(void); watch_date_time_t movement_get_utc_date_time(void);
watch_date_time_t movement_get_local_date_time(void); watch_date_time_t movement_get_local_date_time(void);
watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index); watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index);
uint32_t movement_get_utc_timestamp(void);
void movement_set_utc_date_time(watch_date_time_t date_time);
void movement_set_local_date_time(watch_date_time_t date_time); void movement_set_local_date_time(watch_date_time_t date_time);
void movement_set_utc_timestamp(uint32_t timestamp);
bool movement_button_should_sound(void); bool movement_button_should_sound(void);
void movement_set_button_should_sound(bool value); void movement_set_button_should_sound(bool value);
@@ -362,12 +348,6 @@ void movement_set_button_should_sound(bool value);
watch_buzzer_volume_t movement_button_volume(void); watch_buzzer_volume_t movement_button_volume(void);
void movement_set_button_volume(watch_buzzer_volume_t value); void movement_set_button_volume(watch_buzzer_volume_t value);
watch_buzzer_volume_t movement_signal_volume(void);
void movement_set_signal_volume(watch_buzzer_volume_t value);
watch_buzzer_volume_t movement_alarm_volume(void);
void movement_set_alarm_volume(watch_buzzer_volume_t value);
movement_clock_mode_t movement_clock_mode_24h(void); movement_clock_mode_t movement_clock_mode_24h(void);
void movement_set_clock_mode_24h(movement_clock_mode_t value); void movement_set_clock_mode_24h(movement_clock_mode_t value);
+3 -11
View File
@@ -32,13 +32,13 @@ const watch_face_t watch_faces[] = {
world_clock_face, world_clock_face,
sunrise_sunset_face, sunrise_sunset_face,
moon_phase_face, moon_phase_face,
fast_stopwatch_face, stopwatch_face,
countdown_face, countdown_face,
alarm_face, alarm_face,
temperature_display_face, temperature_display_face,
voltage_face, voltage_face,
settings_face, settings_face,
set_time_face, set_time_face
}; };
#define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t))
@@ -49,7 +49,7 @@ const watch_face_t watch_faces[] = {
* Some folks also like to use this to hide the preferences and time set faces from the normal rotation. * Some folks also like to use this to hide the preferences and time set faces from the normal rotation.
* If you don't want any faces to be excluded, set this to 0 and a long Mode press will have no effect. * If you don't want any faces to be excluded, set this to 0 and a long Mode press will have no effect.
*/ */
#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 5) #define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 4)
/* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */ /* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */
#define SIGNAL_TUNE_DEFAULT #define SIGNAL_TUNE_DEFAULT
@@ -68,8 +68,6 @@ const watch_face_t watch_faces[] = {
#define MOVEMENT_DEFAULT_BUTTON_SOUND true #define MOVEMENT_DEFAULT_BUTTON_SOUND true
#define MOVEMENT_DEFAULT_BUTTON_VOLUME WATCH_BUZZER_VOLUME_SOFT #define MOVEMENT_DEFAULT_BUTTON_VOLUME WATCH_BUZZER_VOLUME_SOFT
#define MOVEMENT_DEFAULT_SIGNAL_VOLUME WATCH_BUZZER_VOLUME_LOUD
#define MOVEMENT_DEFAULT_ALARM_VOLUME WATCH_BUZZER_VOLUME_LOUD
/* Set the timeout before switching back to the main watch face /* Set the timeout before switching back to the main watch face
* Valid values are: * Valid values are:
@@ -102,10 +100,4 @@ const watch_face_t watch_faces[] = {
*/ */
#define MOVEMENT_DEFAULT_LED_DURATION 1 #define MOVEMENT_DEFAULT_LED_DURATION 1
/* Optionally debounce button presses (disable by default).
* A value of 4 is a good starting point if you have issues
* with multiple button presses firing.
*/
#define MOVEMENT_DEBOUNCE_TICKS 0
#endif // MOVEMENT_CONFIG_H_ #endif // MOVEMENT_CONFIG_H_
-6
View File
@@ -79,10 +79,4 @@
#include "higher_lower_game_face.h" #include "higher_lower_game_face.h"
#include "lander_face.h" #include "lander_face.h"
#include "simon_face.h" #include "simon_face.h"
#include "ping_face.h"
#include "rtccount_face.h"
#include "tomato_face.h"
#include "solar_time_face.h"
#include "tide_face.h"
#include "wyoscan_face.h"
// New includes go above this line. // New includes go above this line.
-6
View File
@@ -23,7 +23,6 @@ SRCS += \
./watch-faces/demo/character_set_face.c \ ./watch-faces/demo/character_set_face.c \
./watch-faces/demo/light_sensor_face.c \ ./watch-faces/demo/light_sensor_face.c \
./watch-faces/demo/peek_memory_face.c \ ./watch-faces/demo/peek_memory_face.c \
./watch-faces/demo/rtccount_face.c \
./watch-faces/sensor/accelerometer_status_face.c \ ./watch-faces/sensor/accelerometer_status_face.c \
./watch-faces/sensor/temperature_display_face.c \ ./watch-faces/sensor/temperature_display_face.c \
./watch-faces/sensor/temperature_logging_face.c \ ./watch-faces/sensor/temperature_logging_face.c \
@@ -55,9 +54,4 @@ SRCS += \
./watch-faces/complication/higher_lower_game_face.c \ ./watch-faces/complication/higher_lower_game_face.c \
./watch-faces/complication/lander_face.c \ ./watch-faces/complication/lander_face.c \
./watch-faces/complication/simon_face.c \ ./watch-faces/complication/simon_face.c \
./watch-faces/complication/ping_face.c \
./watch-faces/complication/tomato_face.c \
./watch-faces/clock/solar_time_face.c \
./watch-faces/complication/tide_face.c \
./watch-faces/clock/wyoscan_face.c \
# New watch faces go above this line. # New watch faces go above this line.
+12 -12
View File
@@ -40,20 +40,20 @@ static const char *words[12] = {
" ", " ",
" 5", " 5",
"10", "10",
"CU", "15",
"20", "20",
"Me", "25",
"ME", "30",
"mE", "35",
"40", "40",
"45", "45",
"50", "50",
"55", "55",
}; };
static const char *past_word = " y"; static const char *past_word = " P";
static const char *to_word = " -"; static const char *to_word = " 2";
static const char *oclock_word = "EP"; static const char *oclock_word = "OC";
// sets when in the five minute period we switch // sets when in the five minute period we switch
// from "X past HH" to "X to HH+1" // from "X past HH" to "X to HH+1"
@@ -87,7 +87,7 @@ void close_enough_face_activate(void *context) {
clock_stop_tick_tock_animation(); clock_stop_tick_tock_animation();
clock_indicate(WATCH_INDICATOR_SIGNAL, movement_alarm_enabled()); clock_indicate(WATCH_INDICATOR_BELL, movement_alarm_enabled());
clock_indicate(WATCH_INDICATOR_24H, !!movement_clock_mode_24h()); clock_indicate(WATCH_INDICATOR_24H, !!movement_clock_mode_24h());
// this ensures that none of the five_minute_periods will match, so we always rerender when the face activates // this ensures that none of the five_minute_periods will match, so we always rerender when the face activates
@@ -189,14 +189,14 @@ bool close_enough_face_loop(movement_event_t event, void *context) {
char second_word[3]; char second_word[3];
char third_word[3]; char third_word[3];
if (five_minute_period == 0) { // " HH OC", if (five_minute_period == 0) { // " HH OC",
sprintf(first_word, "%2d", close_enough_hour); sprintf(first_word, " ");
sprintf(second_word, " "); sprintf(second_word, "%2d", close_enough_hour);
strncpy(third_word, oclock_word, 3); strncpy(third_word, oclock_word, 3);
} else { // "MM P HH" or "MM 2 HH+1" } else { // "MM P HH" or "MM 2 HH+1"
int words_length = sizeof(words) / sizeof(words[0]); int words_length = sizeof(words) / sizeof(words[0]);
strncpy( strncpy(
third_word, first_word,
show_next_hour ? show_next_hour ?
words[words_length - five_minute_period] : words[words_length - five_minute_period] :
words[five_minute_period], words[five_minute_period],
@@ -207,7 +207,7 @@ bool close_enough_face_loop(movement_event_t event, void *context) {
show_next_hour ? to_word : past_word, show_next_hour ? to_word : past_word,
3 3
); );
sprintf(first_word, "%2d", close_enough_hour); sprintf(third_word, "%2d", close_enough_hour);
} }
watch_display_text_with_fallback( watch_display_text_with_fallback(
-236
View File
@@ -1,236 +0,0 @@
/*
* 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[9];
if (_load_location().reg == 0) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SOL", "SO");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, "no Loc");
watch_clear_colon();
return;
}
switch (state->mode) {
case SOLAR_TIME_MODE_LST: {
int32_t s = _lst_seconds(dt, state->TC);
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SOL", "SO");
watch_display_text(WATCH_POSITION_TOP_RIGHT, "Ar");
sprintf(bottom, "%02d%02d%02d",
(int)(s / 3600), (int)((s % 3600) / 60), (int)(s % 60));
watch_set_colon();
break;
}
case SOLAR_TIME_MODE_NOON: {
/* Solar noon: moment when LST = 12:00 → LT_noon = 12h - TC/60 */
int32_t s = (int32_t)(( 12.0f - state->TC / 60.0f) * 3600.0f);
s = ((s % 86400) + 86400) % 86400;
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "NOO", "NO");
watch_display_text(WATCH_POSITION_TOP_RIGHT, "n ");
sprintf(bottom, "%02d%02d ", (int)(s / 3600), (int)((s % 3600) / 60));
watch_set_colon();
break;
}
case SOLAR_TIME_MODE_HRA: {
/* HRA = 15 * (LST - 12); negative = morning, positive = afternoon */
int32_t s = _lst_seconds(dt, state->TC);
int16_t hra = (int16_t)roundf(15.0f * ((float)s / 3600.0f - 12.0f));
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "HrA", "Hr");
watch_display_text(WATCH_POSITION_TOP_RIGHT, "n ");
sprintf(bottom, "%+4d ", (int)hra);
watch_clear_colon();
break;
}
default:
return;
}
watch_display_text(WATCH_POSITION_BOTTOM, bottom);
}
/* ---- Movement callbacks -------------------------------------------------- */
void solar_time_face_setup(uint8_t watch_face_index, void **context_ptr) {
(void)watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(solar_time_state_t));
memset(*context_ptr, 0, sizeof(solar_time_state_t));
/* last_calc_d == 0 guarantees recomputation on first tick */
}
}
void solar_time_face_activate(void *context) {
solar_time_state_t *state = (solar_time_state_t *)context;
#if __EMSCRIPTEN__
/* In the simulator the browser exposes lat/lon as JS globals.
* Write them to location.u32 if not already set. */
int16_t browser_lat = EM_ASM_INT({ return lat; });
int16_t browser_lon = EM_ASM_INT({ return lon; });
if (browser_lat || browser_lon) {
movement_location_t browser_loc = {0};
filesystem_read_file("location.u32", (char *)&browser_loc.reg, sizeof(browser_loc.reg));
if (browser_loc.reg == 0) {
browser_loc.bit.latitude = browser_lat;
browser_loc.bit.longitude = browser_lon;
filesystem_write_file("location.u32", (char *)&browser_loc.reg, sizeof(browser_loc.reg));
}
}
#endif
/* Force recompute on activation: timezone or location may have changed */
state->last_calc_d = 0;
watch_date_time_t dt = movement_get_local_date_time();
_maybe_recompute(state, dt);
}
bool solar_time_face_loop(movement_event_t event, void *context) {
solar_time_state_t *state = (solar_time_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
case EVENT_TICK: {
watch_date_time_t dt = movement_get_local_date_time();
_maybe_recompute(state, dt);
_update_display(state, dt);
break;
}
case EVENT_ALARM_BUTTON_UP:
state->mode = (solar_time_mode_t)((state->mode + 1) % SOLAR_TIME_NUM_MODES);
{
watch_date_time_t dt = movement_get_local_date_time();
_update_display(state, dt);
}
break;
case EVENT_LOW_ENERGY_UPDATE: {
if (!watch_sleep_animation_is_running()) watch_start_sleep_animation(1000);
watch_date_time_t dt = movement_get_local_date_time();
_maybe_recompute(state, dt);
_update_display(state, dt);
break;
}
case EVENT_TIMEOUT:
state->mode = SOLAR_TIME_MODE_LST;
if (_load_location().reg == 0) movement_move_to_face(0);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void solar_time_face_resign(void *context) {
solar_time_state_t *state = (solar_time_state_t *)context;
state->mode = SOLAR_TIME_MODE_LST;
watch_clear_colon();
}
-84
View File
@@ -1,84 +0,0 @@
/*
* 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, \
})
+3 -1
View File
@@ -41,7 +41,9 @@
* to the time zone setting, and press ALARM to cycle through the available time * to the time zone setting, and press ALARM to cycle through the available time
* zones. Press LIGHT one last time to return to the world clock display. * zones. Press LIGHT one last time to return to the world clock display.
* *
* Note that the second slot cannot display all letters or numbers. * Note that the second slot cannot display all letters or numbers. Also note
* that at this time, time zones do not automatically update for daylight saving
* time; you will need to manually adjust this field each spring and fall.
*/ */
#include "movement.h" #include "movement.h"
-1
View File
@@ -1 +0,0 @@
totp_face.secrets.h
+12 -11
View File
@@ -169,7 +169,7 @@ static void _alarm_update_alarm_enabled(alarm_state_t *state) {
uint16_t now_minutes_of_day; uint16_t now_minutes_of_day;
uint16_t alarm_minutes_of_day; uint16_t alarm_minutes_of_day;
for (uint8_t i = 0; i < ALARM_ALARMS; i++) { for (uint8_t i = 0; i < ALARM_ALARMS; i++) {
if (state->alarm[i].enabled && state->alarm[i].beeps != 0) { if (state->alarm[i].enabled) {
// figure out if alarm is to go off in the next 24 h // figure out if alarm is to go off in the next 24 h
if (state->alarm[i].day == ALARM_DAY_EACH_DAY || state->alarm[i].day == ALARM_DAY_ONE_TIME) { if (state->alarm[i].day == ALARM_DAY_EACH_DAY || state->alarm[i].day == ALARM_DAY_ONE_TIME) {
active_alarms = true; active_alarms = true;
@@ -202,16 +202,9 @@ static void _alarm_update_alarm_enabled(alarm_state_t *state) {
static void _alarm_play_short_beep(uint8_t pitch_idx) { static void _alarm_play_short_beep(uint8_t pitch_idx) {
// play a short double beep // play a short double beep
static int8_t beep_sequence[] = { watch_buzzer_play_note(_buzzer_notes[pitch_idx], 50);
0, 4, watch_buzzer_play_note(BUZZER_NOTE_REST, 50);
BUZZER_NOTE_REST, 4, watch_buzzer_play_note(_buzzer_notes[pitch_idx], 70);
0, 6,
0
};
beep_sequence[0] = _buzzer_notes[pitch_idx];
beep_sequence[4] = _buzzer_notes[pitch_idx];
movement_play_sequence(beep_sequence, 0);
} }
static void _alarm_indicate_beep(alarm_state_t *state) { static void _alarm_indicate_beep(alarm_state_t *state) {
@@ -270,6 +263,7 @@ void advanced_alarm_face_resign(void *context) {
alarm_state_t *state = (alarm_state_t *)context; alarm_state_t *state = (alarm_state_t *)context;
state->is_setting = false; state->is_setting = false;
_alarm_update_alarm_enabled(state); _alarm_update_alarm_enabled(state);
watch_set_led_off();
state->alarm_quick_ticks = false; state->alarm_quick_ticks = false;
_wait_ticks = -1; _wait_ticks = -1;
movement_request_tick_frequency(1); movement_request_tick_frequency(1);
@@ -443,7 +437,14 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) {
// play alarm // play alarm
if (state->alarm[state->alarm_playing_idx].beeps == 0) { if (state->alarm[state->alarm_playing_idx].beeps == 0) {
// short beep // short beep
if (watch_is_buzzer_or_led_enabled()) {
_alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch); _alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch);
} else {
// enable, play beep and disable buzzer again
watch_enable_buzzer();
_alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch);
watch_disable_buzzer();
}
} else { } else {
// regular alarm beeps // regular alarm beeps
movement_play_alarm_beeps((state->alarm[state->alarm_playing_idx].beeps == (ALARM_MAX_BEEP_ROUNDS - 1) ? 20 : state->alarm[state->alarm_playing_idx].beeps), movement_play_alarm_beeps((state->alarm[state->alarm_playing_idx].beeps == (ALARM_MAX_BEEP_ROUNDS - 1) ? 20 : state->alarm[state->alarm_playing_idx].beeps),
+2 -3
View File
@@ -338,11 +338,10 @@ bool countdown_face_loop(movement_event_t event, void *context) {
// Toggle auto-repeat // Toggle auto-repeat
button_beep(); button_beep();
state->repeat = !state->repeat; state->repeat = !state->repeat;
watch_indicator_t repeat = watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC ? WATCH_INDICATOR_BELL : WATCH_INDICATOR_LAP;
if(state->repeat) if(state->repeat)
watch_set_indicator(repeat); watch_set_indicator(WATCH_INDICATOR_BELL);
else else
watch_clear_indicator(repeat); watch_clear_indicator(WATCH_INDICATOR_BELL);
} }
break; break;
case EVENT_ALARM_LONG_UP: case EVENT_ALARM_LONG_UP:
+7 -17
View File
@@ -82,36 +82,26 @@ static inline int _days_in_month(int16_t month, int16_t year)
/* Play beep sound based on type */ /* Play beep sound based on type */
static inline void _beep(beep_type_t beep_type) static inline void _beep(beep_type_t beep_type)
{ {
static int8_t beep_sequence[] = {
0, 4,
0, 6,
0, 6,
0
};
if (!movement_button_should_sound()) if (!movement_button_should_sound())
return; return;
switch (beep_type) { switch (beep_type) {
case BEEP_BUTTON: case BEEP_BUTTON:
beep_sequence[0] = BUZZER_NOTE_C7; watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume());
beep_sequence[2] = 0;
break; break;
case BEEP_ENABLE: case BEEP_ENABLE:
beep_sequence[0] = BUZZER_NOTE_G7; watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume());
beep_sequence[2] = BUZZER_NOTE_REST; watch_buzzer_play_note(BUZZER_NOTE_REST, 75);
beep_sequence[4] = BUZZER_NOTE_C8; watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume());
break; break;
case BEEP_DISABLE: case BEEP_DISABLE:
beep_sequence[0] = BUZZER_NOTE_C8; watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume());
beep_sequence[2] = BUZZER_NOTE_REST; watch_buzzer_play_note(BUZZER_NOTE_REST, 75);
beep_sequence[4] = BUZZER_NOTE_G7; watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume());
break; break;
} }
movement_play_sequence(beep_sequence, 0);
} }
/* Change tick frequency */ /* Change tick frequency */
+222 -239
View File
@@ -2,7 +2,6 @@
* MIT License * MIT License
* *
* Copyright (c) 2022 Andreas Nebinger * Copyright (c) 2022 Andreas Nebinger
* Copyright (c) 2025 Alessandro Genova
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -25,13 +24,11 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <limits.h>
#include "fast_stopwatch_face.h" #include "fast_stopwatch_face.h"
#include "watch.h" #include "watch.h"
#include "watch_common_display.h" #include "watch_common_display.h"
#include "watch_utility.h" #include "watch_utility.h"
#include "watch_rtc.h" #include "watch_rtc.h"
#include "slcd.h"
/* /*
This watch face implements the original F-91W stopwatch functionality This watch face implements the original F-91W stopwatch functionality
@@ -43,247 +40,173 @@
turns on on each button press or it doesn't. turns on on each button press or it doesn't.
*/ */
// Loosely implement the watch as a state machine #if __EMSCRIPTEN__
typedef enum { #include <emscripten.h>
SW_STATUS_IDLE = 0, #include <emscripten/html5.h>
SW_STATUS_RUNNING, #else
SW_STATUS_RUNNING_LAPPING, #include "tc.h"
SW_STATUS_STOPPED, #endif
SW_STATUS_STOPPED_LAPPING
} stopwatch_status_t; // 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
static inline void _button_beep() { static inline void _button_beep() {
// play a beep as confirmation for a button press (if applicable) // play a beep as confirmation for a button press (if applicable)
if (movement_button_should_sound()) watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume()); if (movement_button_should_sound()) watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume());
} }
// How quickly should the elapsing time be displayed?
// This is just for looks, timekeeping is always accurate to 128Hz
static const uint8_t DISPLAY_RUNNING_RATE = 32;
static const uint8_t DISPLAY_RUNNING_RATE_SLOW = 2;
/// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter /// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter
/// on the lcd. /// on the lcd.
/// @param ticks /// @param ticks
static void _display_elapsed(fast_stopwatch_state_t *state, uint32_t ticks) { static void _display_ticks(uint32_t ticks) {
char buf[3]; char buf[14];
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; uint8_t sec_100 = (ticks & 0x7F) * 100 / 128;
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
}
uint32_t seconds = ticks >> 7; uint32_t seconds = ticks >> 7;
if (seconds == state->old_display.seconds) {
return;
}
state->old_display.seconds = seconds;
sprintf(buf, "%02lu", seconds % 60);
watch_display_text(WATCH_POSITION_MINUTES, buf);
uint32_t minutes = seconds / 60; uint32_t minutes = seconds / 60;
if (_hours) {
if (minutes == state->old_display.minutes) { sprintf(buf, "%2u", _hours);
return;
}
state->old_display.minutes = minutes;
sprintf(buf, "%02lu", minutes % 60);
watch_display_text(WATCH_POSITION_HOURS, buf);
uint32_t hours = (minutes / 60) % 24;
if (hours == state->old_display.hours) {
return;
}
state->old_display.hours = hours;
if (hours) {
sprintf(buf, "%2lu", hours);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
} else { } else {
watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
} }
sprintf(buf, "%02lu%02lu%02u", minutes, (seconds % 60), sec_100);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
} }
static void _draw_indicators(fast_stopwatch_state_t *state, movement_event_t event, uint32_t elapsed) { /// @brief Displays the current stopwatch time on the LCD (more optimized than _display_ticks())
uint8_t subsecond; static void _draw() {
bool tock; if (_lap_ticks == 0) {
char buf[14];
switch (state->status) { uint8_t sec_100 = (_ticks & 0x7F) * 100 / 128;
case SW_STATUS_RUNNING: if (_is_running) {
subsecond = elapsed & 127; uint32_t seconds = _ticks >> 7;
tock = subsecond >= 64; if (seconds != _old_seconds) {
// seconds have changed
watch_clear_indicator(WATCH_INDICATOR_LAP); _old_seconds = seconds;
if (tock) { uint8_t minutes = seconds / 60;
watch_clear_colon(); 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 { } else {
watch_set_colon(); // no hour indicator
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
} }
sprintf(buf, "%02u%02lu%02u", minutes, seconds, sec_100);
return; watch_display_text(WATCH_POSITION_BOTTOM, buf);
case SW_STATUS_RUNNING_LAPPING:
tock = event.subsecond > 0;
if (tock) {
watch_clear_indicator(WATCH_INDICATOR_LAP);
watch_clear_colon();
} else { } else {
watch_set_indicator(WATCH_INDICATOR_LAP); // just draw seconds
watch_set_colon(); sprintf(buf, "%02lu", seconds);
// note that we're drawing the seconds in the "minutes" position, since this
// watch face uses the "seconds" position for hundredths of seconds
watch_display_text(WATCH_POSITION_MINUTES, buf);
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
} }
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 { } else {
return DISPLAY_RUNNING_RATE; // 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();
} }
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) { static inline void _update_lap_indicator() {
switch (state->status) { if (_lap_ticks) watch_set_indicator(WATCH_INDICATOR_LAP);
case SW_STATUS_IDLE: else watch_clear_indicator(WATCH_INDICATOR_LAP);
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) { static inline void _set_colon() {
switch (state->status) { watch_set_colon();
case SW_STATUS_IDLE: _colon = true;
return 0;
case SW_STATUS_RUNNING:
return counter - state->start_counter;
case SW_STATUS_RUNNING_LAPPING:
case SW_STATUS_STOPPED_LAPPING:
return state->lap_counter - state->start_counter;
case SW_STATUS_STOPPED:
return state->stop_counter - state->start_counter;
default:
return 0;
}
} }
void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) { void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
@@ -292,54 +215,114 @@ void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
*context_ptr = malloc(sizeof(fast_stopwatch_state_t)); *context_ptr = malloc(sizeof(fast_stopwatch_state_t));
memset(*context_ptr, 0, sizeof(fast_stopwatch_state_t)); memset(*context_ptr, 0, sizeof(fast_stopwatch_state_t));
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)*context_ptr; fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)*context_ptr;
state->start_counter = 0; _ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0;
state->stop_counter = 0; _is_running = _colon = false;
state->lap_counter = 0; state->light_on_button = true;
state->status = SW_STATUS_IDLE; }
if (!_is_running) {
// prepare the 128 Hz callback source
_cb_initialize();
} }
} }
void fast_stopwatch_face_activate(void *context) { void fast_stopwatch_face_activate(void *context) {
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *) context; (void) context;
// force full re-draw if (_is_running) {
state->old_display.seconds = UINT_MAX; // The background task will keep the watch from entering low energy mode while the stopwatch is on screen.
state->old_display.minutes = UINT_MAX; movement_schedule_background_task(distant_future);
state->old_display.hours = UINT_MAX; }
movement_request_tick_frequency(get_refresh_rate(state));
} }
bool fast_stopwatch_face_loop(movement_event_t event, void *context) { bool fast_stopwatch_face_loop(movement_event_t event, void *context) {
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)context; fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)context;
rtc_counter_t counter = watch_rtc_get_counter(); // handle overflow of fast ticks
while (_ticks >= (128 * 60 * 60)) {
state_transition(state, counter, event.event_type); _ticks -= (128 * 60 * 60);
rtc_counter_t elapsed = elapsed_time(state, counter); _hours++;
if (_hours >= 24) _hours -= 24;
// initiate a re-draw
_old_minutes = 59;
}
switch (event.event_type) { switch (event.event_type) {
case EVENT_ACTIVATE: case EVENT_ACTIVATE:
_set_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST"); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST");
_draw_indicators(state, event, elapsed); _update_lap_indicator();
_display_elapsed(state, elapsed); 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();
break; break;
case EVENT_ALARM_BUTTON_DOWN: case EVENT_ALARM_BUTTON_DOWN:
case EVENT_LIGHT_BUTTON_DOWN: _is_running = !_is_running;
case EVENT_LIGHT_LONG_PRESS: 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(); _button_beep();
// fall through break;
case EVENT_TICK: case EVENT_LIGHT_BUTTON_DOWN:
_draw_indicators(state, event, elapsed); if (state->light_on_button) movement_illuminate_led();
_display_elapsed(state, elapsed); if (_is_running) {
if (_lap_ticks) {
// clear lap and continue running
_lap_ticks = 0;
movement_request_tick_frequency(16);
} else {
// set lap ticks and stop updating the display
_lap_ticks = _ticks;
movement_request_tick_frequency(2);
_set_colon();
}
} else {
if (_lap_ticks) {
// clear lap and show running stopwatch
_lap_ticks = 0;
} else if (_ticks) {
// reset stopwatch
_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0;
_button_beep();
}
}
_display_ticks(_ticks);
_update_lap_indicator();
break;
case EVENT_TIMEOUT:
if (!_is_running) movement_move_to_face(0);
break;
case EVENT_LOW_ENERGY_UPDATE:
_draw();
break; break;
default: default:
movement_default_loop_handler(event); movement_default_loop_handler(event);
break; break;
} }
return true; return true;
} }
void fast_stopwatch_face_resign(void *context) { void fast_stopwatch_face_resign(void *context) {
(void) context; (void) context;
movement_request_tick_frequency(1); // cancel the keepalive task
movement_cancel_background_task();
} }
+7 -10
View File
@@ -55,16 +55,7 @@
#include "movement.h" #include "movement.h"
typedef struct { typedef struct {
rtc_counter_t start_counter; // rtc counter when the stopwatch was started bool light_on_button; // determines whether the light button actually triggers the led
rtc_counter_t lap_counter; // rtc counter when the stopwatch was lapped
rtc_counter_t stop_counter; // rtc counter when the stopwatch was stopped
uint8_t status; // the status the stopwatch is in (idle, running, stopped)
bool slow_refresh; // update the display slowly (same 128Hz timekeeping accuracy)
struct {
rtc_counter_t seconds;
rtc_counter_t minutes;
rtc_counter_t hours;
} old_display; // the digits currently being displayed on screen
} fast_stopwatch_state_t; } fast_stopwatch_state_t;
void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr); void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr);
@@ -72,6 +63,12 @@ void fast_stopwatch_face_activate(void *context);
bool fast_stopwatch_face_loop(movement_event_t event, void *context); bool fast_stopwatch_face_loop(movement_event_t event, void *context);
void fast_stopwatch_face_resign(void *context); void fast_stopwatch_face_resign(void *context);
#if __EMSCRIPTEN__
void em_cb_handler(void *userData);
#else
void TC2_Handler(void);
#endif
#define fast_stopwatch_face ((const watch_face_t){ \ #define fast_stopwatch_face ((const watch_face_t){ \
fast_stopwatch_face_setup, \ fast_stopwatch_face_setup, \
fast_stopwatch_face_activate, \ fast_stopwatch_face_activate, \
+2 -1
View File
@@ -96,7 +96,8 @@ static inline void _inc_uint8(uint8_t *value, uint8_t step, uint8_t max) {
static uint32_t _get_now_ts() { static uint32_t _get_now_ts() {
// returns the current date time as unix timestamp // returns the current date time as unix timestamp
return movement_get_utc_timestamp(); watch_date_time_t now = watch_rtc_get_date_time();
return watch_utility_date_time_to_unix_time(now, 0);
} }
static inline void _button_beep() { static inline void _button_beep() {
+26 -26
View File
@@ -72,41 +72,41 @@ static void _update(moon_phase_state_t *state, uint32_t offset) {
switch (phase_index) { switch (phase_index) {
case 0: case 0:
case 8: case 8:
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "NE!J ", " NUEVA"); watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "NE!J ", " Neu ");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, " ", " "); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, " ", " ");
break; break;
case 1: case 1:
watch_display_text(WATCH_POSITION_BOTTOM, "Crecie"); watch_display_text(WATCH_POSITION_BOTTOM, "CresNt");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WAX", " "); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WAX", " ");
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_set_pixel(0, 14); watch_set_pixel(2, 13);
watch_set_pixel(0, 13); watch_set_pixel(2, 15);
if (currentfrac > 0.125) watch_set_pixel(2, 14); if (currentfrac > 0.125) watch_set_pixel(1, 13);
} }
break; break;
case 2: case 2:
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "1stQtr", "4#Crec"); watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "1stQtr", " 1st q");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WAX", " "); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WAX", " ");
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_set_pixel(2, 13);
watch_set_pixel(2, 15);
watch_set_pixel(1, 13);
watch_set_pixel(1, 14); watch_set_pixel(1, 14);
watch_set_pixel(2, 14);
watch_set_pixel(0, 14);
watch_set_pixel(0, 13);
} }
break; break;
case 3: case 3:
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "GbboUs", " GibCr"); watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "GbboUs", " Gibb ");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WAX", " "); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WAX", " ");
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_set_pixel(2, 13);
watch_set_pixel(2, 15);
watch_set_pixel(1, 14); watch_set_pixel(1, 14);
watch_set_pixel(2, 14); watch_set_pixel(1, 13);
watch_set_pixel(1, 15); watch_set_pixel(1, 15);
watch_set_pixel(0, 14);
watch_set_pixel(0, 13);
} }
break; break;
case 4: case 4:
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "FULL ", "LLENA "); watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "FULL ", " FULL ");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, " ", " "); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, " ", " ");
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_set_pixel(2, 13); watch_set_pixel(2, 13);
@@ -120,33 +120,33 @@ static void _update(moon_phase_state_t *state, uint32_t offset) {
} }
break; break;
case 5: case 5:
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "GbboUs", " GibMe"); watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "GbboUs", " Gibb ");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WAN", " "); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WAN", " ");
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_set_pixel(2, 13);
watch_set_pixel(2, 15);
watch_set_pixel(1, 14); watch_set_pixel(1, 14);
watch_set_pixel(1, 13); watch_set_pixel(2, 14);
watch_set_pixel(1, 15); watch_set_pixel(1, 15);
watch_set_pixel(0, 14);
watch_set_pixel(0, 13);
} }
break; break;
case 6: case 6:
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "3rdQtr", "4#meng"); watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "3rdQtr", " 3rd q");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WAN", " "); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WAN", " ");
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_set_pixel(2, 13);
watch_set_pixel(2, 15);
watch_set_pixel(1, 13);
watch_set_pixel(1, 14); watch_set_pixel(1, 14);
watch_set_pixel(2, 14);
watch_set_pixel(0, 14);
watch_set_pixel(0, 13);
} }
break; break;
case 7: case 7:
watch_display_text(WATCH_POSITION_BOTTOM, "MENGUA"); watch_display_text(WATCH_POSITION_BOTTOM, "CresNt");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WAN", " "); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WAN", " ");
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_set_pixel(2, 13); watch_set_pixel(0, 14);
watch_set_pixel(2, 15); watch_set_pixel(0, 13);
if (currentfrac < 0.875) watch_set_pixel(1, 13); if (currentfrac < 0.875) watch_set_pixel(2, 14);
} }
break; break;
} }
-583
View File
@@ -1,583 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2024 <David Volovskiy>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include <string.h>
#include "ping_face.h"
#include "delay.h"
#include "watch_common_display.h"
typedef enum {
PADDLE_RETRACTED = 0,
PADDLE_EXTENDING,
PADDLE_EXTENDED,
PADDLE_RETRACTING,
} PingPaddleState;
typedef enum {
SCREEN_TITLE = 0,
SCREEN_SCORE,
SCREEN_PLAYING,
SCREEN_LOSE,
SCREEN_COUNT
} PingCurrScreen;
typedef enum {
DIFF_BABY = 0, // FREQ_BABY FPS
DIFF_EASY, // FREQ_EASY FPS
DIFF_NORM, // FREQ_NORM FPS
DIFF_HARD, // FREQ_NORM FPS, smaller travel-distance for ball
DIFF_COUNT
} PingDifficulty;
typedef enum {
RESULT_LOSE = -1,
RESULT_NONE = 0,
RESULT_HIT = 1,
RESULT_FIRST_HIT = 2,
} PingResult;
#define FREQ_BABY 2
#define FREQ_EASY 4
#define FREQ_NORM 8
#define BALL_POS_MAX 11
#define BALL_OFF_SCREEN 100
#define MAX_HI_SCORE 9999 // Max hi score to store and display on the title screen.
#define MAX_DISP_SCORE 39 // The top-right digits can't properly display above 39
typedef struct {
uint8_t ball_pos; // 0 to 11; 0 is the bottom-right and 11 is the top right.
// | 6 | 7 | 8 | 9 | 10 | 11 |
// | 5 | 4 | 3 | 2 | 1 | 0 |
PingPaddleState paddle_pos;
uint8_t ball_char_pos; // Derived from ball_pos
bool ball_is_clockwise;
bool ball_is_moving;
uint16_t curr_score;
PingCurrScreen curr_screen;
bool paddle_hit;
bool paddle_released;
uint8_t curr_freq;
bool moving_from_tap;
} game_state_t;
static game_state_t game_state;
static int8_t _ticks_show_title = 0;
static bool _is_custom_lcd;
static int8_t start_tune[] = {
BUZZER_NOTE_C5, 15,
BUZZER_NOTE_E5, 15,
BUZZER_NOTE_G5, 15,
0
};
static int8_t lose_tune[] = {
BUZZER_NOTE_D3, 10,
BUZZER_NOTE_C3SHARP_D3FLAT, 10,
BUZZER_NOTE_C3, 10,
0
};
static uint8_t ball_pos_to_char_pos(uint8_t ball_pos) {
switch (ball_pos)
{
case 5:
case 6:
return 4;
case 4:
case 7:
return 5;
case 3:
case 8:
return 6;
case 2:
case 9:
return 7;
case 1:
case 10:
return 8;
case 0:
case 11:
return 9;
default:
return BALL_OFF_SCREEN;
}
}
static bool paddle_and_ball_on_same_segment(void) {
if (game_state.paddle_pos == PADDLE_EXTENDED) {
if (game_state.ball_pos == 9 || game_state.ball_pos == 2) {
return true;
}
}
else if (game_state.paddle_pos == PADDLE_EXTENDING || game_state.paddle_pos == PADDLE_RETRACTING) {
if (game_state.ball_pos == 10 || game_state.ball_pos == 1) {
return true;
}
}
else if (game_state.paddle_pos == PADDLE_RETRACTED) {
if (game_state.ball_pos == 11 || game_state.ball_pos == 0) {
return true;
}
}
return false;
}
static bool paddle_hit_ball(void) {
if (game_state.paddle_pos == PADDLE_EXTENDED) {
if (game_state.ball_pos >= 9 && game_state.ball_is_clockwise) {
return true;
}
if (game_state.ball_pos <= 2 && !game_state.ball_is_clockwise) {
return true;
}
}
else if (game_state.paddle_pos == PADDLE_EXTENDING) {
if (game_state.ball_pos >= 10 && game_state.ball_is_clockwise) {
return true;
}
if (game_state.ball_pos <= 1 && !game_state.ball_is_clockwise) {
return true;
}
}
return false;
}
static uint8_t get_next_ball_pos(bool ball_hit, uint8_t difficulty) {
int8_t offset_next;
if (ball_hit) {
bool ball_on_top = game_state.ball_pos > 5;
game_state.ball_is_clockwise = !ball_on_top;
// ball is at the same frame as the paddle
if (game_state.paddle_pos == PADDLE_EXTENDED) {
return ball_on_top ? 9 : 2;
} else if (game_state.paddle_pos == PADDLE_EXTENDING) {
return ball_on_top ? 10 : 1;
}
}
if (game_state.ball_is_clockwise) {
offset_next = 1;
} else {
offset_next = -1;
}
int8_t next_pos = game_state.ball_pos + offset_next;
if (next_pos > BALL_POS_MAX || next_pos < 0) {
return BALL_OFF_SCREEN;
}
if (difficulty == DIFF_HARD) {
if (next_pos == 4) {
next_pos = 8;
} else if (next_pos == 7) {
next_pos = 3;
}
}
return next_pos;
}
static void display_ball(void) {
uint8_t char_pos = ball_pos_to_char_pos(game_state.ball_pos);
uint8_t char_display;
bool overlap = paddle_and_ball_on_same_segment();
if (game_state.ball_pos > 5) {
if (overlap) {
char_display = 'q';
} else {
char_display = '#';
}
} else {
if (!_is_custom_lcd && (char_pos == 4 || char_pos == 6)) {
char_display = 'n'; // No need to check for overlap on these segments
} else {
if (overlap) {
char_display = 'd';
} else {
char_display = 'o';
}
}
}
watch_display_character(char_display, char_pos);
}
static PingResult update_ball(uint8_t difficulty) {
bool ball_hit = paddle_hit_ball();
bool first_hit = false;
if (!game_state.ball_is_moving) {
if (ball_hit) {
game_state.ball_is_moving = true;
first_hit = true;
} else {
return RESULT_NONE;
}
}
game_state.ball_pos = get_next_ball_pos(ball_hit, difficulty);
if (game_state.ball_pos == BALL_OFF_SCREEN) {
return RESULT_LOSE;
}
display_ball();
if (ball_hit) {
return first_hit ? RESULT_FIRST_HIT : RESULT_HIT;
} else {
return RESULT_NONE;
}
}
static void display_paddle(void) {
switch (game_state.paddle_pos)
{
case PADDLE_EXTENDING:
case PADDLE_RETRACTING:
watch_display_character('-', 9);
watch_display_character('1', 8);
break;
case PADDLE_EXTENDED:
watch_display_character('-', 9);
watch_display_character('-', 8);
watch_display_character('1', 7);
break;
case PADDLE_RETRACTED:
default:
watch_display_character('1', 9);
break;
}
}
static void update_paddle(void) {
switch (game_state.paddle_pos)
{
case PADDLE_RETRACTED:
if (game_state.paddle_hit) {
game_state.paddle_pos = PADDLE_EXTENDING;
}
break;
case PADDLE_EXTENDING:
if (!game_state.moving_from_tap && !HAL_GPIO_BTN_ALARM_read()) {
game_state.paddle_pos = PADDLE_RETRACTED;
watch_display_character(' ', 8);
game_state.moving_from_tap = false;
} else {
game_state.paddle_pos = PADDLE_EXTENDED;
}
break;
case PADDLE_EXTENDED:
game_state.paddle_pos = PADDLE_RETRACTING;
watch_display_character(' ', 7);
break;
case PADDLE_RETRACTING:
game_state.paddle_pos = PADDLE_RETRACTED;
watch_display_character(' ', 8);
game_state.moving_from_tap = false;
break;
default:
break;
}
game_state.paddle_hit = false;
display_paddle();
}
static inline bool paddle_is_extending(void) {
return game_state.paddle_pos == PADDLE_EXTENDING || game_state.paddle_pos == PADDLE_EXTENDED;
}
static void display_score(uint8_t score) {
char buf[3];
score %= (MAX_DISP_SCORE + 1);
sprintf(buf, "%2d", score);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
static void add_to_score(ping_state_t *state) {
if (game_state.curr_score <= MAX_HI_SCORE) {
game_state.curr_score++;
if (game_state.curr_score > state -> hi_score)
state -> hi_score = game_state.curr_score;
}
display_score(game_state.curr_score);
}
static void check_and_reset_hi_score(ping_state_t *state) {
// Resets the hi score at the beginning of each month.
watch_date_time_t date_time = movement_get_local_date_time();
if ((state -> year_last_hi_score != date_time.unit.year) ||
(state -> month_last_hi_score != date_time.unit.month))
{
// The high score resets itself every new month.
state -> hi_score = 0;
state -> year_last_hi_score = date_time.unit.year;
state -> month_last_hi_score = date_time.unit.month;
}
}
static void display_difficulty(uint16_t difficulty) {
static const char *labels[] = {
[DIFF_BABY] = " b",
[DIFF_EASY] = " E",
[DIFF_NORM] = " N",
[DIFF_HARD] = " H"
};
watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]);
}
static void change_difficulty(ping_state_t *state) {
state -> difficulty = (state -> difficulty + 1) % DIFF_COUNT;
display_difficulty(state -> difficulty);
if (state -> soundOn) {
if (state -> difficulty == 0) watch_buzzer_play_note(BUZZER_NOTE_B4, 30);
else watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
}
}
static void display_sound_indicator(bool soundOn) {
if (soundOn) {
watch_set_indicator(WATCH_INDICATOR_BELL);
} else {
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
}
static void toggle_sound(ping_state_t *state) {
state -> soundOn = !state -> soundOn;
display_sound_indicator(state -> soundOn);
if (state -> soundOn) {
watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
}
}
static void enable_tap_control(ping_state_t *state) {
if (!state->tap_control_on) {
movement_enable_tap_detection_if_available();
state->tap_control_on = true;
}
}
static void disable_tap_control(ping_state_t *state) {
if (state->tap_control_on) {
movement_disable_tap_detection_if_available();
state->tap_control_on = false;
}
}
static void display_title(ping_state_t *state) {
movement_request_tick_frequency(1);
game_state.curr_screen = SCREEN_TITLE;
watch_clear_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP, "Ping", "PI ");
watch_display_text(WATCH_POSITION_BOTTOM, " Ping ");
display_sound_indicator(state -> soundOn);
_ticks_show_title = 1;
}
static void display_score_screen(ping_state_t *state) {
uint16_t hi_score = state -> hi_score;
uint8_t difficulty = state -> difficulty;
movement_request_tick_frequency(1);
bool sound_on = state -> soundOn;
memset(&game_state, 0, sizeof(game_state));
game_state.curr_screen = SCREEN_SCORE;
watch_set_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP, "PI ", "PI ");
if (hi_score > MAX_HI_SCORE) {
watch_display_text(WATCH_POSITION_BOTTOM, "HS --");
}
else {
char buf[10];
sprintf(buf, "HS%4d", hi_score);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
display_difficulty(difficulty);
display_sound_indicator(sound_on);
}
static void begin_playing(ping_state_t *state) {
game_state.curr_screen = SCREEN_PLAYING;
watch_clear_colon();
display_sound_indicator(state -> soundOn);
switch (state -> difficulty)
{
case DIFF_BABY:
game_state.curr_freq = FREQ_BABY;
break;
case DIFF_EASY:
game_state.curr_freq = FREQ_EASY;
break;
case DIFF_NORM:
case DIFF_HARD:
default:
game_state.curr_freq = FREQ_NORM;
break;
}
movement_request_tick_frequency(game_state.curr_freq);
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, " ");
game_state.paddle_pos = PADDLE_RETRACTED;
game_state.ball_pos = 1;
game_state.paddle_hit = false;
game_state.ball_is_moving = false;
game_state.ball_is_clockwise = false;
game_state.curr_score = 0;
display_paddle();
display_ball();
display_score( game_state.curr_score);
}
static void display_lose_screen(ping_state_t *state) {
game_state.curr_screen = SCREEN_LOSE;
game_state.curr_score = 0;
watch_clear_display();
watch_display_text(WATCH_POSITION_BOTTOM, " LOSE ");
if (state -> soundOn) {
watch_buzzer_play_sequence(lose_tune, NULL);
delay_ms(600);
}
}
static void update_game(ping_state_t *state) {
if (game_state.ball_is_moving) {
watch_display_character(' ', ball_pos_to_char_pos(game_state.ball_pos)); // remove the old ball.
}
update_paddle();
int game_result = update_ball(state -> difficulty);
if (game_result == RESULT_LOSE) {
display_lose_screen(state);
} else if (game_result == RESULT_HIT) {
add_to_score(state);
if (state -> soundOn) {
watch_buzzer_play_note(BUZZER_NOTE_C5, 60);
}
} else if (game_result == RESULT_FIRST_HIT && state -> soundOn) {
watch_buzzer_play_sequence(start_tune, NULL);
}
}
void ping_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(ping_state_t));
memset(*context_ptr, 0, sizeof(ping_state_t));
ping_state_t *state = (ping_state_t *)*context_ptr;
state->difficulty = DIFF_NORM;
state->tap_control_on = false;
}
}
void ping_face_activate(void *context) {
(void) context;
_is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM;
if (watch_sleep_animation_is_running()) {
watch_stop_blink();
}
}
bool ping_face_loop(movement_event_t event, void *context) {
ping_state_t *state = (ping_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
disable_tap_control(state);
check_and_reset_hi_score(state);
display_title(state);
break;
case EVENT_TICK:
switch (game_state.curr_screen)
{
case SCREEN_TITLE:
if (_ticks_show_title > 0) {_ticks_show_title--;}
else {
watch_clear_display();
display_score_screen(state);
}
case SCREEN_SCORE:
case SCREEN_LOSE:
break;
case SCREEN_PLAYING:
default:
update_game(state);
break;
}
break;
case EVENT_ALARM_BUTTON_UP:
case EVENT_LIGHT_BUTTON_UP:
switch (game_state.curr_screen) {
case SCREEN_SCORE:
enable_tap_control(state);
begin_playing(state);
break;
case SCREEN_TITLE:
enable_tap_control(state);
// fall through
case SCREEN_LOSE:
watch_clear_display();
display_score_screen(state);
default:
break;
}
break;
case EVENT_LIGHT_LONG_PRESS:
if (game_state.curr_screen == SCREEN_SCORE)
change_difficulty(state);
break;
case EVENT_SINGLE_TAP:
case EVENT_DOUBLE_TAP:
// Allow starting a new game by tapping.
if (game_state.curr_screen == SCREEN_SCORE) {
begin_playing(state);
break;
}
else if (game_state.curr_screen == SCREEN_LOSE) {
display_score_screen(state);
break;
}
else if (game_state.curr_screen == SCREEN_PLAYING) {
game_state.moving_from_tap = true;
game_state.paddle_hit = true;
}
break;
case EVENT_ALARM_BUTTON_DOWN:
if (game_state.curr_screen == SCREEN_PLAYING) {
game_state.moving_from_tap = false;
game_state.paddle_hit = true;
}
break;
case EVENT_ALARM_LONG_PRESS:
if (game_state.curr_screen == SCREEN_TITLE || game_state.curr_screen == SCREEN_SCORE)
toggle_sound(state);
break;
case EVENT_TIMEOUT:
disable_tap_control(state);
if (game_state.curr_screen != SCREEN_SCORE) {
display_score_screen(state);
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void ping_face_resign(void *context) {
ping_state_t *state = (ping_state_t *)context;
disable_tap_control(state);
}
-71
View File
@@ -1,71 +0,0 @@
/*
* 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_
+14 -5
View File
@@ -119,22 +119,30 @@ static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_res
switch (note) { switch (note) {
case SIMON_LED_NOTE: case SIMON_LED_NOTE:
if (!state->lightOff) watch_set_led_yellow(); if (!state->lightOff) watch_set_led_yellow();
if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep); if (state->soundOff)
delay_ms(_delay_beep); delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
break; break;
case SIMON_MODE_NOTE: case SIMON_MODE_NOTE:
if (!state->lightOff) watch_set_led_red(); if (!state->lightOff) watch_set_led_red();
if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep); if (state->soundOff)
delay_ms(_delay_beep); delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep);
break; break;
case SIMON_ALARM_NOTE: case SIMON_ALARM_NOTE:
if (!state->lightOff) watch_set_led_green(); if (!state->lightOff) watch_set_led_green();
if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep); if (state->soundOff)
delay_ms(_delay_beep); delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep);
break; break;
case SIMON_WRONG_NOTE: case SIMON_WRONG_NOTE:
if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_A1, 800); if (state->soundOff)
delay_ms(800); delay_ms(800);
else
watch_buzzer_play_note(BUZZER_NOTE_A1, 800);
break; break;
} }
watch_set_led_off(); watch_set_led_off();
@@ -142,7 +150,7 @@ static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_res
if (note != SIMON_WRONG_NOTE) { if (note != SIMON_WRONG_NOTE) {
_simon_clear_display(state); _simon_clear_display(state);
if (!skip_rest) { if (!skip_rest) {
delay_ms((_delay_beep * 2)/3); watch_buzzer_play_note(BUZZER_NOTE_REST, (_delay_beep * 2)/3);
} }
} }
} }
@@ -323,5 +331,6 @@ bool simon_face_loop(movement_event_t event,
void simon_face_resign(void *context) { void simon_face_resign(void *context) {
(void)context; (void)context;
watch_set_led_off();
watch_set_buzzer_off(); watch_set_buzzer_off();
} }
+12 -8
View File
@@ -93,9 +93,13 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) {
double lat = (double)lat_centi / 100.0; double lat = (double)lat_centi / 100.0;
double lon = (double)lon_centi / 100.0; double lon = (double)lon_centi / 100.0;
// sunriset returns the rise/set times as signed decimal hours in UTC.
// this can mean hours below 0 or above 31, which won't fit into a watch_date_time_t struct.
// to deal with this, we set aside the offset in hours, and add it back before converting it to a watch_date_time_t.
double hours_from_utc = ((double)movement_get_current_timezone_offset()) / 3600.0;
// we loop twice because if it's after sunset today, we need to recalculate to display values for tomorrow. // we loop twice because if it's after sunset today, we need to recalculate to display values for tomorrow.
for(int i = 0; i < 2; i++) { for(int i = 0; i < 2; i++) {
double hours_from_utc = ((double)movement_get_timezone_offset_for_date(scratch_time)) / 3600.0;
uint8_t result = sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &rise, &set); uint8_t result = sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &rise, &set);
if (result != 0) { if (result != 0) {
@@ -104,7 +108,7 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) {
watch_clear_indicator(WATCH_INDICATOR_24H); watch_clear_indicator(WATCH_INDICATOR_24H);
if (result == 1) watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SET", "SE"); if (result == 1) watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SET", "SE");
else watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "RIS", "rI"); else watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "RIS", "rI");
snprintf(buf, sizeof(buf), "%2d", scratch_time.unit.day); sprintf(buf, "%2d", scratch_time.unit.day);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
watch_display_text(WATCH_POSITION_BOTTOM, "None "); watch_display_text(WATCH_POSITION_BOTTOM, "None ");
return; return;
@@ -145,9 +149,9 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) {
else watch_clear_indicator(WATCH_INDICATOR_PM); else watch_clear_indicator(WATCH_INDICATOR_PM);
} }
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "RIS", "rI"); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "RIS", "rI");
snprintf(buf, sizeof(buf), "%2d", scratch_time.unit.day); sprintf(buf, "%2d", scratch_time.unit.day);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
snprintf(buf, sizeof(buf), "%2d%02d%2s", scratch_time.unit.hour, scratch_time.unit.minute,longLatPresets[state->longLatToUse].name); sprintf(buf, "%2d%02d%2s", scratch_time.unit.hour, scratch_time.unit.minute,longLatPresets[state->longLatToUse].name);
watch_display_text(WATCH_POSITION_BOTTOM, buf); watch_display_text(WATCH_POSITION_BOTTOM, buf);
return; return;
} else { } else {
@@ -184,9 +188,9 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) {
else watch_clear_indicator(WATCH_INDICATOR_PM); else watch_clear_indicator(WATCH_INDICATOR_PM);
} }
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SET", "SE"); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SET", "SE");
snprintf(buf, sizeof(buf), "%2d", scratch_time.unit.day); sprintf(buf, "%2d", scratch_time.unit.day);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
snprintf(buf, sizeof(buf), "%2d%02d%2s", scratch_time.unit.hour, scratch_time.unit.minute,longLatPresets[state->longLatToUse].name); sprintf(buf, "%2d%02d%2s", scratch_time.unit.hour, scratch_time.unit.minute,longLatPresets[state->longLatToUse].name);
watch_display_text(WATCH_POSITION_BOTTOM, buf); watch_display_text(WATCH_POSITION_BOTTOM, buf);
return; return;
} else { } else {
@@ -270,7 +274,7 @@ static void _sunrise_sunset_face_update_settings_display(movement_event_t event,
if (state->active_digit == 4) watch_display_character(' ', 9); if (state->active_digit == 4) watch_display_character(' ', 9);
} }
} else { } else {
snprintf(buf, sizeof(buf), "%c %04d", state->working_latitude.sign ? '-' : '+', abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude))); sprintf(buf, "%c %04d", state->working_latitude.sign ? '-' : '+', abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude)));
if (event.subsecond % 2) buf[state->active_digit] = ' '; if (event.subsecond % 2) buf[state->active_digit] = ' ';
watch_display_text(WATCH_POSITION_BOTTOM, buf); watch_display_text(WATCH_POSITION_BOTTOM, buf);
} }
@@ -297,7 +301,7 @@ static void _sunrise_sunset_face_update_settings_display(movement_event_t event,
if (state->active_digit == 4) watch_display_character(' ', 9); if (state->active_digit == 4) watch_display_character(' ', 9);
} }
} else { } else {
snprintf(buf, sizeof(buf), "%c%05d", state->working_longitude.sign ? '-' : '+', abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude))); sprintf(buf, "%c%05d", state->working_longitude.sign ? '-' : '+', abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude)));
if (event.subsecond % 2) buf[state->active_digit] = ' '; if (event.subsecond % 2) buf[state->active_digit] = ' ';
watch_display_text(WATCH_POSITION_BOTTOM, buf); watch_display_text(WATCH_POSITION_BOTTOM, buf);
} }
+6 -16
View File
@@ -110,12 +110,6 @@ static bool tally_face_should_move_back(tally_state_t *state) {
bool tally_face_loop(movement_event_t event, void *context) { bool tally_face_loop(movement_event_t event, void *context) {
tally_state_t *state = (tally_state_t *)context; tally_state_t *state = (tally_state_t *)context;
static bool using_led = false; static bool using_led = false;
static int8_t beep_sequence[] = {
0, 2,
BUZZER_NOTE_REST, 3,
0, 2,
0
};
if (using_led) { if (using_led) {
if(!HAL_GPIO_BTN_MODE_read() && !HAL_GPIO_BTN_LIGHT_read() && !HAL_GPIO_BTN_ALARM_read()) if(!HAL_GPIO_BTN_MODE_read() && !HAL_GPIO_BTN_LIGHT_read() && !HAL_GPIO_BTN_ALARM_read())
@@ -154,11 +148,9 @@ bool tally_face_loop(movement_event_t event, void *context) {
state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index
_init_val = true; _init_val = true;
//play a reset tune //play a reset tune
if (movement_button_should_sound()) { if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30);
beep_sequence[0] = BUZZER_NOTE_G6; if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30);
beep_sequence[4] = BUZZER_NOTE_E6; if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
movement_play_sequence(beep_sequence, 0);
}
print_tally(state, movement_button_should_sound()); print_tally(state, movement_button_should_sound());
} }
break; break;
@@ -176,11 +168,9 @@ bool tally_face_loop(movement_event_t event, void *context) {
if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){ if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){
state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE(); state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE();
state->tally_idx = _tally_default[state->tally_default_idx]; state->tally_idx = _tally_default[state->tally_default_idx];
if (movement_button_should_sound()) { if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
beep_sequence[0] = BUZZER_NOTE_E6; if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30);
beep_sequence[4] = BUZZER_NOTE_G6; if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30);
movement_play_sequence(beep_sequence, 0);
}
print_tally(state, movement_button_should_sound()); print_tally(state, movement_button_should_sound());
} }
else{ else{
-405
View File
@@ -1,405 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2025 Mathias Kende
*
* 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 <math.h>
#include <stdlib.h>
#include <string.h>
#include "tide_face.h"
#include "watch.h"
#include "watch_common_display.h"
#include "watch_utility.h"
// Parameters taken from the moon_phase_face.c file.
#define LUNAR_DAYS 29.53058770576
#define FIRST_MOON 947182440 // Saturday, 6 January 2000 18:14:00 in unix epoch time
#define SEMI_DIURNAL_TIDAL_PERIOD (LUNAR_DAYS / (LUNAR_DAYS - 1) * 12 * 3600) // 12h25m in seconds
#ifndef M_PI
#define M_PI 3.14159265358979
#endif
typedef enum {
TIDE_SPRING, // Less than 1.8 days away from a full or new moon.
TIDE_NEAP, // Less than 1.8 days away from a first or third quarter moon.
TIDE_MEDIUM, // The rest
} tide_amplitude_t;
static tide_amplitude_t _get_tide_amplitude(uint32_t time) {
// Moon age in days, looped over beetween new and full moon (so age is 14.7 days at most).
double moon_age = fmod(((double)(time - FIRST_MOON)) / 86400, LUNAR_DAYS / 2);
if (moon_age <= LUNAR_DAYS / 16 || moon_age >= LUNAR_DAYS * 7 / 16) {
return TIDE_SPRING;
} else if (moon_age > LUNAR_DAYS * 3 / 16 && moon_age < LUNAR_DAYS * 5 / 16) {
return TIDE_NEAP;
} else {
return TIDE_MEDIUM;
}
}
typedef enum {
TIDE_SCREEN_EMPTY, // No tide data set.
TIDE_SCREEN_CURRENT, // Default screen, showing the current tide.
TIDE_SCREEN_FUTURE, // Screen showing the time of future high and low tides.
TIDE_SCREEN_SETTING_HOUR, // Setting screen, setting the hour of the next high tide.
TIDE_SCREEN_SETTING_MIN, // Setting screen, setting the minute of the next high tide.
} tide_mode_t;
typedef enum {
TIDE_HIGH,
TIDE_LOW,
} tide_type_t;
typedef struct {
tide_mode_t mode;
bool start_setting; // we entered the setting mode but did not yet changed any value.
uint32_t next_high_tide;
uint32_t last_current_update_time;
uint32_t future_tide_time;
tide_type_t future_tide_type;
} tide_state_t;
void tide_face_setup(uint8_t watch_face_index, void** state_ptr) {
(void) watch_face_index;
if (*state_ptr == NULL) {
// Boot time initialization.
*state_ptr = malloc(sizeof(tide_state_t));
tide_state_t* state = (tide_state_t*)*state_ptr;
state->mode = TIDE_SCREEN_EMPTY;
}
}
static uint32_t _get_current_unix_time() {
return watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0);
}
static void _move_next_high_tide(tide_state_t* state, uint32_t now) {
while (state->next_high_tide > now + SEMI_DIURNAL_TIDAL_PERIOD) {
state->next_high_tide -= SEMI_DIURNAL_TIDAL_PERIOD;
}
while (state->next_high_tide < now) {
state->next_high_tide += SEMI_DIURNAL_TIDAL_PERIOD;
}
}
void tide_face_activate(void* context) {
tide_state_t* state = (tide_state_t*)context;
if (state->mode != TIDE_SCREEN_EMPTY) {
state->mode = TIDE_SCREEN_CURRENT;
}
// int64 so that the substraction below works (we need a signed number and int32 will overflow soon).
// using int64 everywhere for the unix time would probably be better.
int64_t now = _get_current_unix_time();
if (llabs(now - state->next_high_tide) > 60 * 86400) {
// We revert to the empty mode if the next high tide is more than 2
// months from now, to avoid accumulating too much errors.
state->mode = TIDE_SCREEN_EMPTY;
return;
}
_move_next_high_tide(state, now);
}
static void _set_pixel(segment_mapping_t mapping) {
watch_set_pixel(mapping.address.com, mapping.address.seg);
}
static void _draw_tide_amplitude(uint32_t time) {
const digit_mapping_t* digit_mapping =
watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC ? Classic_LCD_Display_Mapping : Custom_LCD_Display_Mapping;
switch (_get_tide_amplitude(time)) {
case TIDE_SPRING:
_set_pixel(digit_mapping[9].segment[0]); // top horizontal bar on the bottom-right character.
// fall-through
case TIDE_MEDIUM:
_set_pixel(digit_mapping[9].segment[6]); // mid horizontal bar on the bottom-right character.
// fall-through
case TIDE_NEAP:
_set_pixel(digit_mapping[9].segment[3]); // bottom horizontal bar on the bottom-right character.
break;
}
}
static void _draw_day_and_time(uint32_t time, bool show_day, bool show_hour, bool show_minute) {
watch_date_time_t date_time =
watch_utility_date_time_from_unix_time(time, movement_get_current_timezone_offset());
bool pm = false;
if (movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_12H) {
pm = watch_utility_convert_to_12_hour(&date_time);
} else {
watch_set_indicator(WATCH_INDICATOR_24H);
}
if (pm) {
watch_set_indicator(WATCH_INDICATOR_PM);
}
if (show_hour) {
char tide_hour[3];
sprintf(tide_hour, "%2u", date_time.unit.hour);
watch_display_text(WATCH_POSITION_HOURS, tide_hour);
}
if (show_minute) {
char tide_minute[3];
sprintf(tide_minute, "%02u", date_time.unit.minute);
watch_display_text(WATCH_POSITION_MINUTES, tide_minute);
}
if (show_day) {
char tide_day[3];
sprintf(tide_day, "%2u", date_time.unit.day);
watch_display_text(WATCH_POSITION_TOP_RIGHT, tide_day);
}
watch_set_colon();
}
static void _draw(tide_state_t *state, uint32_t now, uint8_t subsecond) {
watch_clear_display();
switch (state->mode) {
case TIDE_SCREEN_EMPTY:
watch_display_text_with_fallback(WATCH_POSITION_TOP, "TIDE", "TI");
watch_display_text(WATCH_POSITION_BOTTOM, "----");
break;
case TIDE_SCREEN_CURRENT: {
double tide_age = state->next_high_tide - now;
_draw_tide_amplitude(now);
double tide_percent = (cos(tide_age / SEMI_DIURNAL_TIDAL_PERIOD * M_PI * 2) + 1) * 50;
if (tide_percent < 5) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "LOW", "LO");
} else if (tide_percent > 95) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "HIGH", "HI");
} else {
if (state->next_high_tide - now < SEMI_DIURNAL_TIDAL_PERIOD / 2) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "FLOOd", "FL");
} else {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "EBB", "EB");
}
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
uint8_t tide_upercent = tide_percent;
char hour[2];
char minute[2];
hour[0] = minute[1] = ' ';
hour[1] = '0' + tide_upercent / 10;
minute[0] = '0' + tide_upercent % 10;
// We use the second hour digit for our first digit, as its
// more capable than the first hour or minute digits.
watch_display_text(WATCH_POSITION_HOURS, hour);
watch_display_text(WATCH_POSITION_MINUTES, minute);
} else {
char tide_text[7];
uint8_t tide_upercent = tide_percent;
sprintf(tide_text, "%2hhu", tide_upercent);
watch_display_text(WATCH_POSITION_HOURS, tide_text);
watch_display_text(WATCH_POSITION_MINUTES, "o#"); // # is rendered as °, o° looks like a percent sign, maybe...
}
}
break;
}
case TIDE_SCREEN_FUTURE:
if (state->future_tide_type == TIDE_LOW) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "LOW", "LO");
} else {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "HIG", "HI");
}
_draw_day_and_time(state->future_tide_time, true, true, true);
_draw_tide_amplitude(state->future_tide_time);
break;
case TIDE_SCREEN_SETTING_HOUR:
case TIDE_SCREEN_SETTING_MIN:
if (state->start_setting) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "HIGH", "HI");
} else {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "HIG", "HI");
}
_draw_day_and_time(state->next_high_tide, !state->start_setting,
(state->mode != TIDE_SCREEN_SETTING_HOUR || subsecond % 2), (state->mode != TIDE_SCREEN_SETTING_MIN || subsecond % 2));
break;
}
}
static void _offset_next_high_tide(tide_state_t* state, int16_t offset) {
state->next_high_tide += offset;
if (state->next_high_tide % 60) {
state->next_high_tide -= state->next_high_tide % 60;
}
state->start_setting = false;
}
bool tide_face_loop(movement_event_t event, void* context) {
tide_state_t* state = (tide_state_t*)context;
uint32_t now = _get_current_unix_time();
// TODO: handle long press in setting mode.
switch (event.event_type) {
case EVENT_ACTIVATE:
_draw(state, now, event.subsecond);
if (state->mode == TIDE_SCREEN_CURRENT) {
state->last_current_update_time = now;
}
break;
case EVENT_TICK:
switch (state->mode) {
case TIDE_SCREEN_CURRENT:
if (now - state->last_current_update_time >= 60) {
_move_next_high_tide(state, now);
_draw(state, now, event.subsecond);
state->last_current_update_time = now;
}
break;
case TIDE_SCREEN_SETTING_HOUR:
case TIDE_SCREEN_SETTING_MIN:
_draw(state, now, event.subsecond);
break;
default:
break;
}
break;
case EVENT_LOW_ENERGY_UPDATE:
_draw(state, now, event.subsecond);
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_start_sleep_animation(500);
} else {
watch_set_indicator(WATCH_INDICATOR_SLEEP);
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
switch (state->mode) {
case TIDE_SCREEN_SETTING_HOUR:
state->mode = TIDE_SCREEN_SETTING_MIN;
_draw(state, now, event.subsecond);
break;
case TIDE_SCREEN_SETTING_MIN:
state->mode = TIDE_SCREEN_CURRENT;
_move_next_high_tide(state, _get_current_unix_time());
movement_request_tick_frequency(1);
_draw(state, now, event.subsecond);
break;
default:
movement_illuminate_led();
break;
}
break;
case EVENT_LIGHT_LONG_PRESS:
if (state->mode == TIDE_SCREEN_FUTURE) {
state->mode = TIDE_SCREEN_CURRENT;
_draw(state, now, event.subsecond);
state->last_current_update_time = now;
}
break;
case EVENT_ALARM_BUTTON_DOWN:
switch(state->mode) {
case TIDE_SCREEN_SETTING_HOUR:
_offset_next_high_tide(state, 3600);
break;
case TIDE_SCREEN_SETTING_MIN:
_offset_next_high_tide(state, 60);
break;
default:
break;
}
_draw(state, now, event.subsecond);
break;
case EVENT_ALARM_BUTTON_UP:
// We react to UP event only so that we dont switch to a TIDE_SCREEN_FUTURE day at the beginning of a long press.
switch(state->mode) {
case TIDE_SCREEN_CURRENT:
if (state->next_high_tide - now > SEMI_DIURNAL_TIDAL_PERIOD / 2) {
state->future_tide_time = state->next_high_tide - SEMI_DIURNAL_TIDAL_PERIOD / 2;
state->future_tide_type = TIDE_LOW;
} else {
state->future_tide_time = state->next_high_tide;
state->future_tide_type = TIDE_HIGH;
}
state->mode = TIDE_SCREEN_FUTURE;
break;
case TIDE_SCREEN_FUTURE:
state->future_tide_time += SEMI_DIURNAL_TIDAL_PERIOD / 2;
state->future_tide_type = state->future_tide_type == TIDE_LOW ? TIDE_HIGH : TIDE_LOW;
break;
default:
break;
}
_draw(state, now, event.subsecond);
break;
case EVENT_ALARM_LONG_PRESS:
switch(state->mode) {
case TIDE_SCREEN_EMPTY:
state->next_high_tide = _get_current_unix_time();
// fall-through
case TIDE_SCREEN_CURRENT:
case TIDE_SCREEN_FUTURE:
state->mode = TIDE_SCREEN_SETTING_HOUR;
state->start_setting = true;
movement_request_tick_frequency(4);
break;
case TIDE_SCREEN_SETTING_HOUR:
case TIDE_SCREEN_SETTING_MIN:
break;
}
_draw(state, now, event.subsecond);
break;
case EVENT_MODE_BUTTON_DOWN:
switch(state->mode) {
case TIDE_SCREEN_SETTING_HOUR:
_offset_next_high_tide(state, -3600);
break;
case TIDE_SCREEN_SETTING_MIN:
_offset_next_high_tide(state, -60);
break;
default:
return movement_default_loop_handler(event);
}
_draw(state, now, event.subsecond);
break;
case EVENT_MODE_BUTTON_UP:
case EVENT_MODE_LONG_PRESS:
switch(state->mode) {
case TIDE_SCREEN_SETTING_HOUR:
case TIDE_SCREEN_SETTING_MIN:
break;
default:
return movement_default_loop_handler(event);
}
break;
case EVENT_TIMEOUT:
if (state->mode == TIDE_SCREEN_SETTING_MIN || state->mode == TIDE_SCREEN_SETTING_HOUR) {
state->mode = TIDE_SCREEN_CURRENT;
_draw(state, now, event.subsecond);
}
// fall-through
// Delegate the resign behavior to the default loop handler.
default:
return movement_default_loop_handler(event);
}
return true;
}
void tide_face_resign(void* context) {
// Any cleanup needed before the watch face goes off-screen.
tide_state_t* state = (tide_state_t*)context;
if (state->mode == TIDE_SCREEN_SETTING_HOUR || state->mode == TIDE_SCREEN_SETTING_MIN) {
// Not strictly needed because it will be done upon re-entering the
// watch face. But lets leave a clean state.
_move_next_high_tide(state, _get_current_unix_time());
}
}
-80
View File
@@ -1,80 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2025 Mathias Kende
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include "movement.h"
/*
* TIDE COMPUTATION face
*
* Computes the time of the next high and low tides in your areas as well as
* their magnitudes and gives an approximation of the current tide.
*
* For now, this face only handle a theoretical perfect semi-diurnal tide,
* similarly to what some Casio watches are doing. In the future, it may be
* possible to handle more precise computation by sending the harmonics
* coeficients of the tides over IR.
*
* To configure the face, long press the Alarm button to enter the setting mode
* then set the time of the high tide in your area. You can move the hour using
* the Alarm and Mode button to go up and down, then press the Light button to
* set the minutes in the same way. Notice that if you overflow the minutes then
* the hour will change. Similarly, when the hours overflow, the day for the
* tide (shown in the upper right corner) is changed too (however there is no
* direct way to set the day). You must be sure to select the right day for the
* tide that you are currently entering.
* Note that, if you set the high tide value for a day with a full or new moon,
* the computation will be slightly more precise.
* Because the Mode button is used, you must first press the Alarm button once
* or twice, to exit the settings, before pressing Mode to exit the watch face.
*
* Once configured, the face start by showing the state of the current tide
* (low, flowing, high, or ebbing) at the top of the screen. If the tide is
* flowing or ebbing, the bottom left part of the screen also shows the current
* hight of the tide as a percentage of the total tide. The bottom right portion
* of the screen shows a representation of whether the current tide is a neap
* or spring tide, or an intermediate one.
*
* You can then repeatedly press the Alarm buttom to see the time of the future
* high and low tides. The bottow right corner has the same representation of
* the amplitude of the tide and the top right corner has the day of the month
* for which the tide is shown.
*
* You can long press the Light button to come back to the state of the current
* tide. Exiting and re-entering the watch face will have the same effect.
*/
void tide_face_setup(uint8_t watch_face_index, void ** context_ptr);
void tide_face_activate(void *context);
bool tide_face_loop(movement_event_t event, void *context);
void tide_face_resign(void *context);
#define tide_face ((const watch_face_t){ \
tide_face_setup, \
tide_face_activate, \
tide_face_loop, \
tide_face_resign, \
NULL, \
})
+1 -1
View File
@@ -57,6 +57,7 @@ static void _start(timer_state_t *state, bool with_beep) {
watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(state->target_ts, movement_get_current_timezone_offset()); watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(state->target_ts, movement_get_current_timezone_offset());
state->mode = running; state->mode = running;
movement_schedule_background_task_for_face(state->watch_face_index, target_dt); movement_schedule_background_task_for_face(state->watch_face_index, target_dt);
watch_set_indicator(WATCH_INDICATOR_BELL);
if (with_beep) watch_buzzer_play_sequence((int8_t *)_sound_seq_start, NULL); if (with_beep) watch_buzzer_play_sequence((int8_t *)_sound_seq_start, NULL);
} }
@@ -85,7 +86,6 @@ static void _draw(timer_state_t *state, uint8_t subsecond) {
min = result.rem; min = result.rem;
h = result.quot; h = result.quot;
sprintf(bottom_time, "%02u%02u%02u", h, min, sec); sprintf(bottom_time, "%02u%02u%02u", h, min, sec);
watch_set_indicator(WATCH_INDICATOR_BELL);
break; break;
case setting: case setting:
if (state->settings_state == 1) { if (state->settings_state == 1) {
-186
View File
@@ -1,186 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2026 Wesley Ellis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include <string.h>
#include "tomato_face.h"
#include "watch.h"
#include "watch_utility.h"
static const uint8_t focus_min = 25;
static const uint8_t break_min = 5;
static uint8_t get_length(tomato_state_t *state) {
if (state->kind == tomato_focus) {
return focus_min;
} else {
return break_min;
}
}
static void tomato_start(tomato_state_t *state) {
uint8_t length = get_length(state);
state->mode = tomato_run;
state->now_ts = movement_get_utc_timestamp();
state->target_ts = watch_utility_offset_timestamp(state->now_ts, 0, length, 0);
watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(state->target_ts, 0);
movement_schedule_background_task_for_face(state->watch_face_index, target_dt);
watch_set_indicator(WATCH_INDICATOR_BELL);
}
static void tomato_draw(tomato_state_t *state) {
char buf[16];
uint32_t delta;
div_t result;
uint8_t min = 0;
uint8_t sec = 0;
char kind;
if (state->kind == tomato_break) {
kind = 'b';
} else {
kind = 'f';
}
switch (state->mode) {
case tomato_run:
if (state->target_ts <= state->now_ts)
delta = 0;
else
delta = state->target_ts - state->now_ts;
result = div(delta, 60);
min = result.quot;
sec = result.rem;
break;
case tomato_ready:
min = get_length(state);
sec = 0;
break;
}
sprintf(buf, " %c", kind);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
sprintf(buf, "%2d%02d%2d", min, sec, state->done_count);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
static void tomato_reset(tomato_state_t *state) {
state->mode = tomato_ready;
movement_cancel_background_task_for_face(state->watch_face_index);
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
static void tomato_ring(tomato_state_t *state) {
movement_play_signal();
tomato_reset(state);
if (state->kind == tomato_focus) {
state->kind = tomato_break;
state->done_count++;
} else {
state->kind = tomato_focus;
}
}
void tomato_face_setup(uint8_t watch_face_index, void ** context_ptr) {
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(tomato_state_t));
tomato_state_t *state = (tomato_state_t *)*context_ptr;
memset(*context_ptr, 0, sizeof(tomato_state_t));
state->mode = tomato_ready;
state->kind = tomato_focus;
state->done_count = 0;
state->watch_face_index = watch_face_index;
}
}
void tomato_face_activate(void *context) {
tomato_state_t *state = (tomato_state_t *)context;
if (state->mode == tomato_run) {
state->now_ts = movement_get_utc_timestamp();
watch_set_indicator(WATCH_INDICATOR_BELL);
}
watch_set_colon();
}
bool tomato_face_loop(movement_event_t event, void *context) {
tomato_state_t *state = (tomato_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
watch_display_text_with_fallback(WATCH_POSITION_TOP, "TOMATO", "TO");
tomato_draw(state);
break;
case EVENT_TICK:
if (state->mode == tomato_run) {
state->now_ts++;
}
tomato_draw(state);
break;
case EVENT_LIGHT_BUTTON_DOWN:
movement_illuminate_led();
if (state->mode == tomato_ready) {
if (state->kind == tomato_break) {
state->kind = tomato_focus;
} else {
state->kind = tomato_break;
}
}
tomato_draw(state);
break;
case EVENT_ALARM_BUTTON_UP:
switch(state->mode) {
case tomato_run:
tomato_reset(state);
break;
case tomato_ready:
tomato_start(state);
break;
}
tomato_draw(state);
break;
case EVENT_ALARM_LONG_PRESS:
state->done_count = 0;
break;
case EVENT_BACKGROUND_TASK:
tomato_ring(state);
tomato_draw(state);
break;
case EVENT_TIMEOUT:
if (state->mode != tomato_run) {
movement_move_to_face(0);
}
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void tomato_face_resign(void *context) {
(void) context;
}
-82
View File
@@ -1,82 +0,0 @@
/*
* 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_
+11 -19
View File
@@ -36,6 +36,7 @@
#include <string.h> #include <string.h>
#include "totp_face.h" #include "totp_face.h"
#include "watch.h" #include "watch.h"
#include "watch_utility.h"
#include "TOTP.h" #include "TOTP.h"
#include "base32.h" #include "base32.h"
@@ -44,7 +45,7 @@
#endif #endif
typedef struct { typedef struct {
const char* labels; unsigned char labels[2];
hmac_alg algorithm; hmac_alg algorithm;
uint32_t period; uint32_t period;
size_t encoded_key_length; size_t encoded_key_length;
@@ -63,14 +64,10 @@ typedef struct {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Enter your TOTP key data below // Enter your TOTP key data below
#if __has_include("totp_face.secrets.h")
#include "totp_face.secrets.h"
#else
static totp_t credentials[] = { static totp_t credentials[] = {
CREDENTIAL(2F, "JBSWY3DPEHPK3PXP", SHA1, 30), CREDENTIAL(2F, "JBSWY3DPEHPK3PXP", SHA1, 30),
CREDENTIAL(AAC, "JBSWY3DPEHPK3PXP", SHA1, 30), CREDENTIAL(AC, "JBSWY3DPEHPK3PXP", SHA1, 30),
}; };
#endif
// END OF KEY DATA. // END OF KEY DATA.
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@@ -124,14 +121,15 @@ static void totp_generate(totp_state_t *totp_state) {
} }
static void totp_display_error(totp_state_t *totp_state) { static void totp_display_error(totp_state_t *totp_state) {
char buf[10 + 1];
totp_t *totp = totp_current(totp_state); totp_t *totp = totp_current(totp_state);
watch_clear_display();
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, totp->labels, totp->labels); snprintf(buf, sizeof(buf), "%c%c ERROR ", totp->labels[0], totp->labels[1]);
watch_display_text(WATCH_POSITION_BOTTOM, "ERROR"); watch_display_text(0, buf);
} }
static void totp_display_code(totp_state_t *totp_state) { static void totp_display_code(totp_state_t *totp_state) {
char buf[7]; char buf[14];
div_t result; div_t result;
uint8_t valid_for; uint8_t valid_for;
totp_t *totp = totp_current(totp_state); totp_t *totp = totp_current(totp_state);
@@ -142,15 +140,9 @@ static void totp_display_code(totp_state_t *totp_state) {
totp_state->steps = result.quot; totp_state->steps = result.quot;
} }
valid_for = totp->period - result.rem; valid_for = totp->period - result.rem;
sprintf(buf, "%c%c%2d%06lu", totp->labels[0], totp->labels[1], valid_for, totp_state->current_code);
watch_clear_display(); watch_display_text(0, buf);
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, totp->labels, totp->labels);
sprintf(buf, "%2d", valid_for);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
sprintf(buf, "%06u", (unsigned int)totp_state->current_code);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
} }
static void totp_display(totp_state_t *totp_state) { static void totp_display(totp_state_t *totp_state) {
@@ -167,7 +159,7 @@ static void totp_generate_and_display(totp_state_t *totp_state) {
} }
static inline uint32_t totp_compute_base_timestamp() { static inline uint32_t totp_compute_base_timestamp() {
return movement_get_utc_timestamp(); return watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0);
} }
void totp_face_setup(uint8_t watch_face_index, void ** context_ptr) { void totp_face_setup(uint8_t watch_face_index, void ** context_ptr) {
+2 -7
View File
@@ -55,17 +55,12 @@
* TOTP credentials. The file includes two examples that you can use as a * TOTP credentials. The file includes two examples that you can use as a
* reference. Credentials are added with the `CREDENTIAL` macro in the form * reference. Credentials are added with the `CREDENTIAL` macro in the form
* `CREDENTIAL(label, key, algorithm, timestep)` where: * `CREDENTIAL(label, key, algorithm, timestep)` where:
* o `label` is a 2 or 3 character label that is displayed in the weekday * o `label` is a 2 character label that is displayed in the weekday digits
* digits to identify the TOTP credential (only the first 2 characters * to identify the TOTP credential.
* are displayed on the classic LCD).
* o `key` is a string with the base32 encoded secret. * o `key` is a string with the base32 encoded secret.
* o `algorithm` is one of the supported hashing algorithms listed above. * o `algorithm` is one of the supported hashing algorithms listed above.
* o `timestep` is how often the TOTP refreshes in seconds. This is usually * o `timestep` is how often the TOTP refreshes in seconds. This is usually
* 30 seconds. * 30 seconds.
* o Alternatively, place the `credentials` variable as-is in a file named
* `totp_face.secrets.h` in this directory, and it will be included instead.
* This lowers the risk of commiting your OTP secrets to GitHub as that file
* is listed in the .gitignore file.
* *
* If you have more than one secret key, press ALARM to cycle through them. * If you have more than one secret key, press ALARM to cycle through them.
* Press LIGHT to cycle in the other direction or keep it pressed longer to * Press LIGHT to cycle in the other direction or keep it pressed longer to
+2 -1
View File
@@ -30,6 +30,7 @@
#include "base32.h" #include "base32.h"
#include "watch.h" #include "watch.h"
#include "watch_utility.h"
#include "filesystem.h" #include "filesystem.h"
#include "totp_lfs_face.h" #include "totp_lfs_face.h"
@@ -252,7 +253,7 @@ void totp_lfs_face_activate(void *context) {
} }
#endif #endif
totp_state->timestamp = movement_get_utc_timestamp(); totp_state->timestamp = watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0);
totp_face_set_record(totp_state, 0); totp_face_set_record(totp_state, 0);
} }
-204
View File
@@ -1,204 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2025 Alessandro Genova
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include <string.h>
#include "rtccount_face.h"
#include "watch.h"
#include "sam.h"
#include "watch_utility.h"
#include "watch_common_display.h"
#include "watch_rtc.h"
typedef enum {
RTCCOUNT_STATUS_COUNTER = 0,
RTCCOUNT_STATUS_COUNTER_SUB,
RTCCOUNT_STATUS_MINUTES,
RTCCOUNT_STATUS_MINUTES_DIFF,
RTCCOUNT_STATUS_NUMBER
} rtccount_face_status_t;
typedef struct {
rtccount_face_status_t status;
uint8_t frequency;
uint32_t n_top_of_minute;
uint32_t ref_timestamp;
} rtccount_state_t;
static const uint32_t COUNTER_MASK = (1 << 19) - 1;
static void _rtccount_face_display_string(char* string, uint8_t pos) {
// watch_display_string is deprecated, but there is no alternative for this use-case
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
watch_display_string(string, pos);
#pragma GCC diagnostic pop
}
static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) {
uint32_t counter = watch_rtc_get_counter();
char buf[11] = " 000000\0";
switch (state->status) {
case RTCCOUNT_STATUS_COUNTER: {
buf[0] = 'C';
break;
}
case RTCCOUNT_STATUS_COUNTER_SUB: {
buf[0] = 'S';
break;
}
case RTCCOUNT_STATUS_MINUTES: {
buf[0] = 'M';
break;
}
case RTCCOUNT_STATUS_MINUTES_DIFF: {
buf[0] = 'D';
break;
}
default:
break;
}
_rtccount_face_display_string(buf, 0);
snprintf(buf, sizeof(buf), "%u", event.subsecond);
uint32_t len = strlen(buf);
_rtccount_face_display_string(buf, 4 - len);
switch (state->status) {
case RTCCOUNT_STATUS_COUNTER: {
snprintf(buf, sizeof(buf), "%lu", counter & COUNTER_MASK);
size_t len = strlen(buf);
_rtccount_face_display_string(buf, 10 - len);
break;
}
case RTCCOUNT_STATUS_COUNTER_SUB: {
snprintf(buf, sizeof(buf), "%lu", counter & 127);
size_t len = strlen(buf);
_rtccount_face_display_string(buf, 10 - len);
break;
}
case RTCCOUNT_STATUS_MINUTES: {
snprintf(buf, sizeof(buf), "%lu", state->n_top_of_minute & COUNTER_MASK);
size_t len = strlen(buf);
_rtccount_face_display_string(buf, 10 - len);
break;
}
case RTCCOUNT_STATUS_MINUTES_DIFF: {
uint32_t elapsed_minutes = (movement_get_utc_timestamp() - state->ref_timestamp) / 60;
snprintf(buf, sizeof(buf), "%lu", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK);
size_t len = strlen(buf);
_rtccount_face_display_string(buf, 10 - len);
break;
}
default:
break;
}
}
void rtccount_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(rtccount_state_t));
memset(*context_ptr, 0, sizeof(rtccount_state_t));
rtccount_state_t *state = (rtccount_state_t *) *context_ptr;
state->status = RTCCOUNT_STATUS_COUNTER;
state->frequency = 1;
state->n_top_of_minute = 0;
rtc_date_time_t datetime = movement_get_utc_date_time();
state->ref_timestamp = movement_get_utc_timestamp() - datetime.unit.second;
}
}
void rtccount_face_activate(void *context) {
rtccount_state_t* state = (rtccount_state_t*)context;
movement_request_tick_frequency(state->frequency);
}
bool rtccount_face_loop(movement_event_t event, void *context) {
rtccount_state_t* state = (rtccount_state_t*)context;
switch (event.event_type) {
case EVENT_BACKGROUND_TASK:
state->n_top_of_minute += 1;
break;
case EVENT_ALARM_BUTTON_UP:
if (state->frequency == 128) {
state->frequency = 1;
} else {
state->frequency *= 2;
}
movement_request_tick_frequency(state->frequency);
break;
case EVENT_ALARM_LONG_PRESS:
state->n_top_of_minute = 0;
rtc_date_time_t datetime = movement_get_utc_date_time();
state->ref_timestamp = movement_get_utc_timestamp() - datetime.unit.second;
break;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = (state->status + 1) % RTCCOUNT_STATUS_NUMBER;
_rtccount_face_draw(event, state);
break;
case EVENT_ACTIVATE:
case EVENT_TICK:
_rtccount_face_draw(event, state);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void rtccount_face_resign(void *context) {
(void) context;
movement_request_tick_frequency(1);
}
movement_watch_face_advisory_t rtccount_face_advise(void *context) {
(void) context;
movement_watch_face_advisory_t retval = { 0 };
retval.wants_background_task = true;
return retval;
}
-47
View File
@@ -1,47 +0,0 @@
/*
* 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, \
})
+103 -45
View File
@@ -47,6 +47,9 @@ typedef struct {
// Selected program // Selected program
chirpy_demo_program_t program; chirpy_demo_program_t program;
// Helps us handle 1/64 ticks during transmission; including countdown timer
chirpy_tick_state_t tick_state;
// Used by chirpy encoder during transmission // Used by chirpy encoder during transmission
chirpy_encoder_state_t encoder_state; chirpy_encoder_state_t encoder_state;
@@ -147,10 +150,46 @@ static void _cdf_update_lcd(chirpy_demo_state_t *state) {
} }
} }
static void _cdf_quit_chirping(chirpy_demo_state_t *state) {
state->mode = CDM_CHOOSE;
watch_set_buzzer_off();
watch_clear_indicator(WATCH_INDICATOR_BELL);
movement_request_tick_frequency(1);
}
static void _cdf_scale_tick(void *context) {
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
chirpy_tick_state_t *tick_state = &state->tick_state;
// Scale goes in 200Hz increments from 700 Hz to 12.3 kHz -> 58 steps
if (tick_state->seq_pos == 58) {
_cdf_quit_chirping(state);
return;
}
uint32_t freq = 700 + tick_state->seq_pos * 200;
uint32_t period = 1000000 / freq;
watch_set_buzzer_period_and_duty_cycle(period, 25);
watch_set_buzzer_on();
++tick_state->seq_pos;
}
static void _cdf_data_tick(void *context) {
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
uint8_t tone = chirpy_get_next_tone(&state->encoder_state);
// Transmission over?
if (tone == 255) {
_cdf_quit_chirping(state);
return;
}
uint16_t period = chirpy_get_tone_period(tone);
watch_set_buzzer_period_and_duty_cycle(period, 25);
watch_set_buzzer_on();
}
static uint8_t *curr_data_ptr; static uint8_t *curr_data_ptr;
static uint16_t curr_data_ix; static uint16_t curr_data_ix;
static uint16_t curr_data_len; static uint16_t curr_data_len;
static chirpy_demo_state_t *curr_state;
static uint8_t _cdf_get_next_byte(uint8_t *next_byte) { static uint8_t _cdf_get_next_byte(uint8_t *next_byte) {
if (curr_data_ix == curr_data_len) if (curr_data_ix == curr_data_len)
@@ -160,46 +199,25 @@ static uint8_t _cdf_get_next_byte(uint8_t *next_byte) {
return 1; return 1;
} }
static void _cdf_on_chirping_done(void) { static void _cdf_countdown_tick(void *context) {
if (curr_state) { chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
curr_state->mode = CDM_CHOOSE; 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;
} }
watch_clear_indicator(WATCH_INDICATOR_BELL); // We'll be chirping out data
} else {
// Set up the encoder
static bool _cdm_raw_source_fn(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration) { chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte);
// Beep countdown tick_state->tick_fun = _cdf_data_tick;
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 // Set up the data
curr_state = state;
curr_data_ix = 0; curr_data_ix = 0;
if (state->program == CDP_INFO_SHORT) { if (state->program == CDP_INFO_SHORT) {
curr_data_ptr = short_data; curr_data_ptr = short_data;
@@ -211,9 +229,29 @@ static void _cdm_start_transmission(chirpy_demo_state_t *state) {
curr_data_ptr = activity_buffer; curr_data_ptr = activity_buffer;
curr_data_len = activity_buffer_size; 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;
}
chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte); static void _cdm_setup_chirp(chirpy_demo_state_t *state) {
watch_buzzer_play_raw_source(_cdm_raw_source_fn, state, _cdf_on_chirping_done); // 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;
} }
bool chirpy_demo_face_loop(movement_event_t event, void *context) { bool chirpy_demo_face_loop(movement_event_t event, void *context) {
@@ -223,7 +261,12 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
case EVENT_ACTIVATE: case EVENT_ACTIVATE:
_cdf_update_lcd(state); _cdf_update_lcd(state);
break; break;
case EVENT_LIGHT_BUTTON_DOWN: 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_UP: case EVENT_LIGHT_BUTTON_UP:
// We don't do light. // We don't do light.
break; break;
@@ -243,6 +286,10 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
state->program = CDP_CLEAR; state->program = CDP_CLEAR;
_cdf_update_lcd(state); _cdf_update_lcd(state);
} }
// If chirping: stoppit
else if (state->mode == CDM_CHIRPING) {
_cdf_quit_chirping(state);
}
break; break;
case EVENT_ALARM_LONG_PRESS: case EVENT_ALARM_LONG_PRESS:
// If in choose mode: start chirping // If in choose mode: start chirping
@@ -252,7 +299,16 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
movement_force_led_off(); movement_force_led_off();
movement_move_to_next_face(); movement_move_to_next_face();
} else { } else {
_cdm_start_transmission(state); _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);
} }
} }
break; break;
@@ -261,12 +317,14 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
if (state->mode != CDM_CHIRPING) { if (state->mode != CDM_CHIRPING) {
movement_move_to_face(0); movement_move_to_face(0);
} }
// fall through
default: default:
movement_default_loop_handler(event);
break; break;
} }
// Return true if the watch can enter standby mode. False needed when chirping.
if (state->mode == CDM_CHIRPING)
return false;
else
return true; return true;
} }
+1 -1
View File
@@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state)
lis2dw_fifo_t fifo; lis2dw_fifo_t fifo;
float x = 0, y = 0, z = 0; float x = 0, y = 0, z = 0;
lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT / DISPLAY_FREQUENCY); lis2dw_read_fifo(&fifo);
if (fifo.count == 0) { if (fifo.count == 0) {
return; return;
} }
+1 -1
View File
@@ -45,7 +45,7 @@ void voltage_face_activate(void *context) {
bool voltage_face_loop(movement_event_t event, void *context) { bool voltage_face_loop(movement_event_t event, void *context) {
(void) context; (void) context;
watch_date_time_t date_time = {0}; watch_date_time_t date_time;
switch (event.event_type) { switch (event.event_type) {
case EVENT_ACTIVATE: case EVENT_ACTIVATE:
if (watch_sleep_animation_is_running()) watch_stop_sleep_animation(); if (watch_sleep_animation_is_running()) watch_stop_sleep_animation();
+16 -7
View File
@@ -27,6 +27,7 @@
#include <math.h> #include <math.h>
#include "finetune_face.h" #include "finetune_face.h"
#include "nanosec_face.h" #include "nanosec_face.h"
#include "watch_utility.h"
#include "delay.h" #include "delay.h"
extern nanosec_state_t nanosec_state; extern nanosec_state_t nanosec_state;
@@ -50,7 +51,7 @@ void finetune_face_activate(void *context) {
} }
static float finetune_get_hours_passed(void) { static float finetune_get_hours_passed(void) {
uint32_t current_time = movement_get_utc_timestamp(); uint32_t current_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0);
return (current_time - nanosec_state.last_correction_time) / 3600.0f; return (current_time - nanosec_state.last_correction_time) / 3600.0f;
} }
@@ -63,7 +64,7 @@ static void finetune_update_display(void) {
if (finetune_page == 0) { if (finetune_page == 0) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "FTU", "FT"); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "FTU", "FT");
watch_date_time_t date_time = movement_get_utc_date_time(); watch_date_time_t date_time = watch_rtc_get_date_time();
sprintf(buf, "%04d%02d", abs(total_adjustment), date_time.unit.second); sprintf(buf, "%04d%02d", abs(total_adjustment), date_time.unit.second);
watch_display_text(WATCH_POSITION_BOTTOM, buf); watch_display_text(WATCH_POSITION_BOTTOM, buf);
@@ -105,9 +106,17 @@ static void finetune_adjust_subseconds(int delta) {
watch_rtc_enable(false); watch_rtc_enable(false);
delay_ms(delta); delay_ms(delta);
if (delta > 500) { if (delta > 500) {
uint32_t timestamp = movement_get_utc_timestamp(); watch_date_time_t date_time = watch_rtc_get_date_time();
timestamp += 1; date_time.unit.second = (date_time.unit.second + 1) % 60;
movement_set_utc_timestamp(timestamp); 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);
} }
watch_rtc_enable(true); watch_rtc_enable(true);
} }
@@ -117,7 +126,7 @@ static void finetune_update_correction_time(void) {
nanosec_state.freq_correction += roundf(nanosec_get_aging() * 100); nanosec_state.freq_correction += roundf(nanosec_get_aging() * 100);
// Remember when we last corrected time // Remember when we last corrected time
nanosec_state.last_correction_time = movement_get_utc_timestamp(); nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0);
nanosec_save(); nanosec_save();
movement_move_to_face(0); // Go to main face after saving settings movement_move_to_face(0); // Go to main face after saving settings
} }
@@ -137,7 +146,7 @@ bool finetune_face_loop(movement_event_t event, void *context) {
// We flash green LED once per minute to measure clock error, when we are not on first screen // We flash green LED once per minute to measure clock error, when we are not on first screen
if (finetune_page!=0) { if (finetune_page!=0) {
watch_date_time_t date_time; watch_date_time_t date_time;
date_time = movement_get_utc_date_time(); date_time = watch_rtc_get_date_time();
if (date_time.unit.second == 0) { if (date_time.unit.second == 0) {
watch_set_led_green(); watch_set_led_green();
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__
+5
View File
@@ -44,6 +44,11 @@
* worry about aging only on second/third years of watch calibration (if you * worry about aging only on second/third years of watch calibration (if you
* are really looking at less than 10 seconds per year of error). * are really looking at less than 10 seconds per year of error).
* *
* Warning, do not use at the first second of a month, as you might stay at
* the same month and it will surprise you. Just wait 1 second...We are not
* fully replicating RTC timer behavior when RTC is off.
* Simulating months and years is... too much complexity.
*
* For full usage instructions, please refer to the wiki: * For full usage instructions, please refer to the wiki:
* https://www.sensorwatch.net/docs/watchfaces/nanosec/ * https://www.sensorwatch.net/docs/watchfaces/nanosec/
*/ */
+6 -4
View File
@@ -27,6 +27,7 @@
#include <math.h> #include <math.h>
#include "nanosec_face.h" #include "nanosec_face.h"
#include "filesystem.h" #include "filesystem.h"
#include "watch_utility.h"
int16_t freq_correction_residual = 0; // Dithering 0.1ppm correction, does not need to be configured. int16_t freq_correction_residual = 0; // Dithering 0.1ppm correction, does not need to be configured.
int16_t freq_correction_previous = -30000; int16_t freq_correction_previous = -30000;
@@ -43,7 +44,8 @@ const float voltage_coefficient = 0.241666667 * dithering; // 10 * ppm/V. Nomina
static void nanosec_init_profile(void) { static void nanosec_init_profile(void) {
nanosec_changed = true; nanosec_changed = true;
nanosec_state.correction_cadence = 10; nanosec_state.correction_cadence = 10;
nanosec_state.last_correction_time = movement_get_utc_timestamp(); 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);
// init data after changing profile - do that once per profile selection // init data after changing profile - do that once per profile selection
switch (nanosec_state.correction_profile) { switch (nanosec_state.correction_profile) {
@@ -263,8 +265,8 @@ static void nanosec_next_edit_screen(void) {
float nanosec_get_aging() // Returns aging correction in ppm float nanosec_get_aging() // Returns aging correction in ppm
{ {
uint32_t timestamp = movement_get_utc_timestamp(); watch_date_time_t date_time = watch_rtc_get_date_time();
float years = (timestamp - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune float years = (watch_utility_date_time_to_unix_time(date_time, 0) - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune
return years*nanosec_state.aging_ppm_pa/100.0f; return years*nanosec_state.aging_ppm_pa/100.0f;
} }
@@ -375,7 +377,7 @@ movement_watch_face_advisory_t nanosec_face_advise(void *context) {
// No need for background correction if we are on profile 0 - static hardware correction. // No need for background correction if we are on profile 0 - static hardware correction.
if (nanosec_state.correction_profile != 0) { if (nanosec_state.correction_profile != 0) {
watch_date_time_t date_time = movement_get_utc_date_time(); watch_date_time_t date_time = watch_rtc_get_date_time();
retval.wants_background_task = date_time.unit.minute % nanosec_state.correction_cadence == 0; retval.wants_background_task = date_time.unit.minute % nanosec_state.correction_cadence == 0;
} }
+5 -1
View File
@@ -108,6 +108,10 @@ bool set_time_face_loop(movement_event_t event, void *context) {
case EVENT_ALARM_LONG_UP: case EVENT_ALARM_LONG_UP:
_abort_quick_ticks(); _abort_quick_ticks();
break; break;
case EVENT_MODE_BUTTON_UP:
_abort_quick_ticks();
movement_move_to_next_face();
return false;
case EVENT_LIGHT_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_DOWN:
current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS; current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS;
*((uint8_t *)context) = current_page; *((uint8_t *)context) = current_page;
@@ -183,6 +187,6 @@ bool set_time_face_loop(movement_event_t event, void *context) {
void set_time_face_resign(void *context) { void set_time_face_resign(void *context) {
(void) context; (void) context;
watch_set_led_off();
movement_store_settings(); movement_store_settings();
movement_request_tick_frequency(1);
} }
+3 -1
View File
@@ -37,7 +37,9 @@
* *
* For features like World Clock and Sunrise/Sunset to work correctly, you * For features like World Clock and Sunrise/Sunset to work correctly, you
* must set the time to your local time, and the time zone to your local time * must set the time to your local time, and the time zone to your local time
* zone. This allows Sensor Watch to correctly offset the time. * zone. This allows Sensor Watch to correctly offset the time. This also
* means that when daylight savings time starts or ends, you must update
* both the time and the time zone on this screen.
*/ */
#include "movement.h" #include "movement.h"
+8 -72
View File
@@ -77,64 +77,6 @@ static void beep_setting_advance(void) {
} }
} }
static void signal_setting_display(uint8_t subsecond) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SIG", "SI");
watch_display_text(WATCH_POSITION_BOTTOM, "SIGNAL");
if (subsecond % 2) {
if (movement_signal_volume() == WATCH_BUZZER_VOLUME_LOUD) {
// H for HIGH
watch_display_text(WATCH_POSITION_TOP_RIGHT, " H");
}
else {
// L for LOW
watch_display_text(WATCH_POSITION_TOP_RIGHT, " L");
}
}
}
static void signal_setting_advance(void) {
if (movement_signal_volume() == WATCH_BUZZER_VOLUME_SOFT) {
// was soft. make it loud.
movement_set_signal_volume(WATCH_BUZZER_VOLUME_LOUD);
} else {
// was loud. make it soft.
movement_set_signal_volume(WATCH_BUZZER_VOLUME_SOFT);
}
signal_setting_display(1);
movement_play_signal();
}
static void alarm_setting_display(uint8_t subsecond) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "ALM", "AL");
watch_display_text(WATCH_POSITION_BOTTOM, "ALARM ");
if (subsecond % 2) {
if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_LOUD) {
// H for HIGH
watch_display_text(WATCH_POSITION_TOP_RIGHT, " H");
}
else {
// L for LOW
watch_display_text(WATCH_POSITION_TOP_RIGHT, " L");
}
}
}
static void alarm_setting_advance(void) {
if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_SOFT) {
// was soft. make it loud.
movement_set_alarm_volume(WATCH_BUZZER_VOLUME_LOUD);
} else {
// was loud. make it soft.
movement_set_alarm_volume(WATCH_BUZZER_VOLUME_SOFT);
}
alarm_setting_display(1);
movement_play_alarm();
}
static void timeout_setting_display(uint8_t subsecond) { static void timeout_setting_display(uint8_t subsecond) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "TMOUt", "TO"); watch_display_text_with_fallback(WATCH_POSITION_TOP, "TMOUt", "TO");
if (subsecond % 2) { if (subsecond % 2) {
@@ -293,10 +235,7 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) {
settings_state_t *state = (settings_state_t *)*context_ptr; settings_state_t *state = (settings_state_t *)*context_ptr;
int8_t current_setting = 0; int8_t current_setting = 0;
state->num_settings = 6; // baseline, without LED settings state->num_settings = 5; // baseline, without LED settings
#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN
state->num_settings++;
#endif
#ifdef BUILD_GIT_HASH #ifdef BUILD_GIT_HASH
state->num_settings++; state->num_settings++;
#endif #endif
@@ -317,12 +256,6 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) {
state->settings_screens[current_setting].display = beep_setting_display; state->settings_screens[current_setting].display = beep_setting_display;
state->settings_screens[current_setting].advance = beep_setting_advance; state->settings_screens[current_setting].advance = beep_setting_advance;
current_setting++; current_setting++;
state->settings_screens[current_setting].display = signal_setting_display;
state->settings_screens[current_setting].advance = signal_setting_advance;
current_setting++;
state->settings_screens[current_setting].display = alarm_setting_display;
state->settings_screens[current_setting].advance = alarm_setting_advance;
current_setting++;
state->settings_screens[current_setting].display = timeout_setting_display; state->settings_screens[current_setting].display = timeout_setting_display;
state->settings_screens[current_setting].advance = timeout_setting_advance; state->settings_screens[current_setting].advance = timeout_setting_advance;
current_setting++; current_setting++;
@@ -378,9 +311,6 @@ bool settings_face_loop(movement_event_t event, void *context) {
settings_state_t *state = (settings_state_t *)context; settings_state_t *state = (settings_state_t *)context;
switch (event.event_type) { switch (event.event_type) {
case EVENT_LIGHT_LONG_PRESS:
movement_illuminate_led();
break;
case EVENT_LIGHT_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_DOWN:
state->current_page = (state->current_page + 1) % state->num_settings; state->current_page = (state->current_page + 1) % state->num_settings;
// fall through // fall through
@@ -389,6 +319,10 @@ bool settings_face_loop(movement_event_t event, void *context) {
watch_clear_display(); watch_clear_display();
state->settings_screens[state->current_page].display(event.subsecond); state->settings_screens[state->current_page].display(event.subsecond);
break; break;
case EVENT_MODE_BUTTON_UP:
movement_force_led_off();
movement_move_to_next_face();
return false;
case EVENT_ALARM_BUTTON_UP: case EVENT_ALARM_BUTTON_UP:
state->settings_screens[state->current_page].advance(); state->settings_screens[state->current_page].advance();
break; break;
@@ -405,13 +339,15 @@ bool settings_face_loop(movement_event_t event, void *context) {
movement_force_led_on(color.red | color.red << 4, movement_force_led_on(color.red | color.red << 4,
color.green | color.green << 4, color.green | color.green << 4,
color.blue | color.blue << 4); color.blue | color.blue << 4);
return true; return false;
} else { } else {
movement_force_led_off();
return true; return true;
} }
} }
void settings_face_resign(void *context) { void settings_face_resign(void *context) {
(void) context; (void) context;
movement_force_led_off();
movement_store_settings(); movement_store_settings();
} }
-6
View File
@@ -45,12 +45,6 @@
* a beep when pressed, and if so, how loud it should be. Options are * a beep when pressed, and if so, how loud it should be. Options are
* "Y" for yes and "N" for no. * "Y" for yes and "N" for no.
* *
* SI / SIG - Signal beep.
* This setting allows you to choose the hourly chime buzzer volume.
*
* AL / ALM - Alarm beep.
* This setting allows you to choose the alarm buzzer volume.
*
* TO / Tmout - Timeout. * TO / Tmout - Timeout.
* Sets the time until screens that time out (like Settings and Time Set) * Sets the time until screens that time out (like Settings and Time Set)
* snap back to the first screen. 60 seconds is a good default for the * snap back to the first screen. 60 seconds is a good default for the
-136
View File
@@ -1,136 +0,0 @@
/*
* 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);
}
+12 -13
View File
@@ -41,7 +41,7 @@ void sleep(const uint8_t mode) {
} }
void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool level) { void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool level) {
uint32_t config = RTC->MODE0.TAMPCTRL.reg; uint32_t config = RTC->MODE2.TAMPCTRL.reg;
if (pin == HAL_GPIO_BTN_ALARM_pin()) { if (pin == HAL_GPIO_BTN_ALARM_pin()) {
HAL_GPIO_BTN_ALARM_in(); HAL_GPIO_BTN_ALARM_in();
@@ -71,22 +71,22 @@ void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool leve
} }
// disable the RTC // disable the RTC
RTC->MODE0.CTRLA.bit.ENABLE = 0; RTC->MODE2.CTRLA.bit.ENABLE = 0;
while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled
// update the configuration // update the configuration
RTC->MODE0.TAMPCTRL.reg = config; RTC->MODE2.TAMPCTRL.reg = config;
// re-enable the RTC // re-enable the RTC
RTC->MODE0.CTRLA.bit.ENABLE = 1; RTC->MODE2.CTRLA.bit.ENABLE = 1;
NVIC_ClearPendingIRQ(RTC_IRQn); NVIC_ClearPendingIRQ(RTC_IRQn);
NVIC_EnableIRQ(RTC_IRQn); NVIC_EnableIRQ(RTC_IRQn);
RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_TAMPER; RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_TAMPER;
} }
void watch_disable_extwake_interrupt(uint8_t pin) { void watch_disable_extwake_interrupt(uint8_t pin) {
uint32_t config = RTC->MODE0.TAMPCTRL.reg; uint32_t config = RTC->MODE2.TAMPCTRL.reg;
if (pin == HAL_GPIO_BTN_ALARM_pin()) { if (pin == HAL_GPIO_BTN_ALARM_pin()) {
btn_alarm_callback = NULL; btn_alarm_callback = NULL;
@@ -101,14 +101,14 @@ void watch_disable_extwake_interrupt(uint8_t pin) {
} }
// disable the RTC // disable the RTC
RTC->MODE0.CTRLA.bit.ENABLE = 0; RTC->MODE2.CTRLA.bit.ENABLE = 0;
while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled
// update the configuration // update the configuration
RTC->MODE0.TAMPCTRL.reg = config; RTC->MODE2.TAMPCTRL.reg = config;
// re-enable the RTC // re-enable the RTC
RTC->MODE0.CTRLA.bit.ENABLE = 1; RTC->MODE2.CTRLA.bit.ENABLE = 1;
} }
void watch_store_backup_data(uint32_t data, uint8_t reg) { void watch_store_backup_data(uint32_t data, uint8_t reg) {
@@ -151,8 +151,7 @@ static void _watch_disable_all_pins_except_rtc(void) {
} }
static void _watch_disable_all_peripherals_except_slcd(void) { static void _watch_disable_all_peripherals_except_slcd(void) {
watch_disable_leds(); _watch_disable_tcc();
watch_disable_buzzer();
watch_disable_adc(); watch_disable_adc();
watch_disable_external_interrupts(); watch_disable_external_interrupts();
+39 -204
View File
@@ -2,7 +2,6 @@
* MIT License * MIT License
* *
* Copyright (c) 2020 Joey Castillo * Copyright (c) 2020 Joey Castillo
* Copyright (c) 2025 Alessandro Genova
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -24,36 +23,11 @@
*/ */
#include <stddef.h> #include <stddef.h>
#include <limits.h>
#include "watch_rtc.h" #include "watch_rtc.h"
#include "watch_private.h" #include "watch_private.h"
#include "watch_utility.h"
static const uint32_t RTC_OSC_DIV = 10;
static const uint32_t RTC_OSC_HZ = 1 << RTC_OSC_DIV; // 2^10 = 1024
static const uint32_t RTC_PRESCALER_DIV = 3;
static const uint32_t RTC_CNT_HZ = RTC_OSC_HZ >> RTC_PRESCALER_DIV; // 1024 / 2^3 = 128
static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1;
static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7
static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60;
static const uint32_t RTC_COMP_GRACE_PERIOD = 4;
static const int TB_BKUP_REG = 7;
#define WATCH_RTC_N_COMP_CB 8
typedef struct {
volatile uint32_t counter;
volatile watch_cb_t callback;
volatile bool enabled;
} comp_cb_t;
volatile uint32_t scheduled_comp_counter;
watch_cb_t tick_callbacks[8]; watch_cb_t tick_callbacks[8];
comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB];
watch_cb_t alarm_callback; watch_cb_t alarm_callback;
watch_cb_t btn_alarm_callback; watch_cb_t btn_alarm_callback;
watch_cb_t a2_callback; watch_cb_t a2_callback;
@@ -72,77 +46,14 @@ void _watch_rtc_init(void) {
#endif #endif
rtc_enable(); rtc_enable();
rtc_configure_callback(watch_rtc_callback); rtc_configure_callback(watch_rtc_callback);
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
comp_callbacks[index].counter = 0;
comp_callbacks[index].callback = NULL;
comp_callbacks[index].enabled = false;
}
scheduled_comp_counter = 0;
NVIC_ClearPendingIRQ(RTC_IRQn);
NVIC_EnableIRQ(RTC_IRQn);
} }
void watch_rtc_set_date_time(rtc_date_time_t date_time) { void watch_rtc_set_date_time(rtc_date_time_t date_time) {
watch_rtc_set_unix_time(watch_utility_date_time_to_unix_time(date_time, 0)); rtc_set_date_time(date_time);
} }
rtc_date_time_t watch_rtc_get_date_time(void) { rtc_date_time_t watch_rtc_get_date_time(void) {
static struct { return rtc_get_date_time();
unix_timestamp_t timestamp;
rtc_date_time_t datetime;
} cached_date_time = {.datetime.reg=0, .timestamp=0};
unix_timestamp_t timestamp = watch_rtc_get_unix_time();
if (timestamp != cached_date_time.timestamp) {
cached_date_time.timestamp = timestamp;
cached_date_time.datetime = watch_utility_date_time_from_unix_time(timestamp, 0);
}
return cached_date_time.datetime;
}
void watch_rtc_set_unix_time(unix_timestamp_t unix_time) {
/* unix_time = time_backup + counter / RTC_CNT_HZ - 0.5
*
* Because of the way the hardware is designed, the periodic interrupts fire at the subsecond tick values
* according to the table below (for a 128Hz counter).
* since the 1Hz periodic interrupt is the most important, we shift the conversion from counter to timestamp by 64 ticks,
* so that the second changes at the top of the 1Hz interrupt. Hence the 0.5 factor in the equation above.
* 1Hz: 64
* 2Hz: 32, 96
* 4Hz: 16, 48, 80, 112
* 8Hz: 8, 24, 40, 56, 72, 88, 104, 120
* 16Hz: 4, 12, 20, ..., 124
* 32Hz: 2, 6, 10, ..., 126
* 64Hz: 1, 3, 5, ..., 127
* 128Hz: 0, 1, 2, ..., 127
*/
rtc_counter_t counter = rtc_get_counter();
unix_timestamp_t tb = unix_time - (counter >> RTC_CNT_DIV) - ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) + 1;
watch_store_backup_data(tb, TB_BKUP_REG);
}
unix_timestamp_t watch_rtc_get_unix_time(void) {
// unix_time = time_backup + counter / RTC_CNT_HZ - 0.5
rtc_counter_t counter = rtc_get_counter();
unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG);
return tb + (counter >> RTC_CNT_DIV) + ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) - 1;
}
rtc_counter_t watch_rtc_get_counter(void) {
return rtc_get_counter();
}
uint32_t watch_rtc_get_frequency(void) {
return RTC_CNT_HZ;
}
uint32_t watch_rtc_get_ticks_per_minute(void) {
return RTC_CNT_TICKS_PER_MINUTE;
} }
rtc_date_time_t watch_get_init_date_time(void) { rtc_date_time_t watch_get_init_date_time(void) {
@@ -192,123 +103,57 @@ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency
// this also maps nicely to an index for our list of tick callbacks. // this also maps nicely to an index for our list of tick callbacks.
tick_callbacks[per_n] = callback; tick_callbacks[per_n] = callback;
// NVIC_ClearPendingIRQ(RTC_IRQn); NVIC_ClearPendingIRQ(RTC_IRQn);
// NVIC_EnableIRQ(RTC_IRQn); NVIC_EnableIRQ(RTC_IRQn);
RTC->MODE0.INTENSET.reg = 1 << per_n; RTC->MODE2.INTENSET.reg = 1 << per_n;
} }
void watch_rtc_disable_periodic_callback(uint8_t frequency) { void watch_rtc_disable_periodic_callback(uint8_t frequency) {
if (__builtin_popcount(frequency) != 1) return; if (__builtin_popcount(frequency) != 1) return;
uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24); uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24);
RTC->MODE0.INTENCLR.reg = 1 << per_n; RTC->MODE2.INTENCLR.reg = 1 << per_n;
} }
void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) { void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) {
RTC->MODE0.INTENCLR.reg = mask; RTC->MODE2.INTENCLR.reg = mask;
} }
void watch_rtc_disable_all_periodic_callbacks(void) { void watch_rtc_disable_all_periodic_callbacks(void) {
watch_rtc_disable_matching_periodic_callbacks(0xFF); watch_rtc_disable_matching_periodic_callbacks(0xFF);
} }
void watch_rtc_schedule_next_comp(void) { void watch_rtc_register_alarm_callback(watch_cb_t callback, rtc_date_time_t alarm_time, rtc_alarm_match_t mask) {
rtc_counter_t curr_counter = watch_rtc_get_counter(); RTC->MODE2.Mode2Alarm[0].ALARM.reg = alarm_time.reg;
RTC->MODE2.Mode2Alarm[0].MASK.reg = mask;
// We want to ensure we never miss any registered callbacks, RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0;
// so if a callback counter has just passed but didn't fire, give it a chance to fire. alarm_callback = callback;
rtc_counter_t lax_curr_counter = curr_counter - RTC_COMP_GRACE_PERIOD; NVIC_ClearPendingIRQ(RTC_IRQn);
NVIC_EnableIRQ(RTC_IRQn);
bool schedule_any = false; RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0;
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_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { void watch_rtc_disable_alarm_callback(void) {
if (index >= WATCH_RTC_N_COMP_CB) { RTC->MODE2.INTENCLR.reg = RTC_MODE2_INTENCLR_ALARM0;
return;
}
comp_callbacks[index].counter = counter;
comp_callbacks[index].callback = callback;
comp_callbacks[index].enabled = true;
watch_rtc_schedule_next_comp();
} }
void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { void watch_rtc_callback(uint16_t interrupt_status) {
if (index >= WATCH_RTC_N_COMP_CB) { uint16_t interrupt_enabled = RTC->MODE2.INTENSET.reg;
return;
}
comp_callbacks[index].counter = counter; if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_PER_Msk) {
comp_callbacks[index].callback = callback;
comp_callbacks[index].enabled = true;
}
void watch_rtc_disable_comp_callback(uint8_t index) {
if (index >= WATCH_RTC_N_COMP_CB) {
return;
}
comp_callbacks[index].enabled = false;
watch_rtc_schedule_next_comp();
}
void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) {
if (index >= WATCH_RTC_N_COMP_CB) {
return;
}
comp_callbacks[index].enabled = false;
}
void watch_rtc_callback(uint16_t interrupt_cause) {
// First read all relevant registers, to ensure no changes occurr during the callbacks
rtc_counter_t curr_counter = watch_rtc_get_counter();
uint16_t interrupt_enabled = (uint16_t)RTC->MODE0.INTENSET.reg;
if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) {
// handle the tick callback first, it's what we do the most. // handle the tick callback first, it's what we do the most.
// start from PER7, the 1 Hz tick. // start from PER7, the 1 Hz tick.
for(int8_t i = 7; i >= 0; i--) { for(int8_t i = 7; i >= 0; i--) {
if ((interrupt_cause & interrupt_enabled) & (1 << i)) { if ((interrupt_status & interrupt_enabled) & (1 << i)) {
if (tick_callbacks[i] != NULL) { if (tick_callbacks[i] != NULL) {
tick_callbacks[i](); tick_callbacks[i]();
} }
RTC->MODE2.INTFLAG.reg = 1 << i;
// break; Uncertain if this fix is requried. We were discussing in discord. Might slightly increase power consumption.
} }
} }
} } else if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_TAMPER) {
if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_TAMPER) {
// handle the extwake interrupts next. // handle the extwake interrupts next.
uint8_t reason = RTC->MODE0.TAMPID.reg; uint8_t reason = RTC->MODE2.TAMPID.reg;
if (reason & RTC_TAMPID_TAMPID2) { if (reason & RTC_TAMPID_TAMPID2) {
if (btn_alarm_callback != NULL) btn_alarm_callback(); if (btn_alarm_callback != NULL) btn_alarm_callback();
} else if (reason & RTC_TAMPID_TAMPID1) { } else if (reason & RTC_TAMPID_TAMPID1) {
@@ -316,36 +161,25 @@ void watch_rtc_callback(uint16_t interrupt_cause) {
} else if (reason & RTC_TAMPID_TAMPID0) { } else if (reason & RTC_TAMPID_TAMPID0) {
if (a4_callback != NULL) a4_callback(); if (a4_callback != NULL) a4_callback();
} }
RTC->MODE0.TAMPID.reg = reason; 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->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) { void watch_rtc_enable(bool en) {
// Writing it twice - as it's quite dangerous operation. // Writing it twice - as it's quite dangerous operation.
// If write fails - we might hang with RTC off, which means no recovery possible // If write fails - we might hang with RTC off, which means no recovery possible
while (RTC->MODE0.SYNCBUSY.reg); while (RTC->MODE2.SYNCBUSY.reg);
RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0; RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0;
while (RTC->MODE0.SYNCBUSY.reg); while (RTC->MODE2.SYNCBUSY.reg);
RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0; RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0;
while (RTC->MODE0.SYNCBUSY.reg); while (RTC->MODE2.SYNCBUSY.reg);
} }
void watch_rtc_freqcorr_write(int16_t value, int16_t sign) { void watch_rtc_freqcorr_write(int16_t value, int16_t sign) {
@@ -354,7 +188,8 @@ void watch_rtc_freqcorr_write(int16_t value, int16_t sign) {
data.bit.VALUE = value; data.bit.VALUE = value;
data.bit.SIGN = sign; data.bit.SIGN = sign;
RTC->MODE0.FREQCORR.reg = data.reg; // Setting correction in single write operation RTC->MODE2.FREQCORR.reg = data.reg; // Setting correction in single write operation
// We do not sycnronize. We are not in a hurry // We do not sycnronize. We are not in a hurry
} }
+54 -219
View File
@@ -27,30 +27,14 @@
#include "tcc.h" #include "tcc.h"
#include "tc.h" #include "tc.h"
static void _watch_enable_tcc(void); void _watch_enable_tcc(void);
static void _watch_disable_tcc(void); void cb_watch_buzzer_seq(void);
static void _watch_maybe_enable_tcc(void);
static void _watch_maybe_disable_tcc(void);
static void _watch_enable_led_pins(void);
static void _watch_disable_led_pins(void);
static void (*_cb_tc0)(void) = NULL;
static void cb_watch_buzzer_seq(void);
static void cb_watch_buzzer_raw_source(void);
static uint16_t _seq_position; static uint16_t _seq_position;
static int8_t _tone_ticks, _repeat_counter; static int8_t _tone_ticks, _repeat_counter;
static bool _callback_running = false;
static int8_t *_sequence; static int8_t *_sequence;
static watch_buzzer_raw_source_t _raw_source;
static void* _userdata;
static uint8_t _volume;
static void (*_cb_finished)(void); static void (*_cb_finished)(void);
static watch_cb_t _cb_start_global = NULL;
static watch_cb_t _cb_stop_global = NULL;
static volatile bool _led_is_active = false;
static volatile bool _buzzer_is_active = false;
static volatile uint8_t _current_led_color[3] = {0, 0, 0};
static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue);
static void _tcc_write_RUNSTDBY(bool value) { static void _tcc_write_RUNSTDBY(bool value) {
// enables or disables RUNSTDBY of the tcc // enables or disables RUNSTDBY of the tcc
@@ -62,11 +46,13 @@ static void _tcc_write_RUNSTDBY(bool value) {
static inline void _tc0_start() { static inline void _tc0_start() {
// start the TC0 timer // start the TC0 timer
tc_enable(0); tc_enable(0);
_callback_running = true;
} }
static inline void _tc0_stop() { static inline void _tc0_stop() {
// stop the TC0 timer // stop the TC0 timer
tc_disable(0); tc_disable(0);
_callback_running = false;
} }
static void _tc0_initialize() { static void _tc0_initialize() {
@@ -82,30 +68,19 @@ static void _tc0_initialize() {
} }
void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) { void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) {
watch_buzzer_play_sequence_with_volume(note_sequence, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); if (_callback_running) _tc0_stop();
}
void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) {
// Abort any previous sequence
watch_buzzer_abort_sequence();
if (_cb_start_global) {
_cb_start_global();
}
watch_enable_buzzer();
watch_set_buzzer_off(); watch_set_buzzer_off();
_sequence = note_sequence; _sequence = note_sequence;
_cb_finished = callback_on_end; _cb_finished = callback_on_end;
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
_seq_position = 0; _seq_position = 0;
_tone_ticks = 0; _tone_ticks = 0;
_repeat_counter = -1; _repeat_counter = -1;
// prepare buzzer // prepare buzzer
watch_enable_buzzer();
_cb_tc0 = cb_watch_buzzer_seq;
// setup TC0 timer // setup TC0 timer
_tc0_initialize(); _tc0_initialize();
// TCC should run in standby mode
_tcc_write_RUNSTDBY(true);
// start the timer (for the 64 hz callback) // start the timer (for the 64 hz callback)
_tc0_start(); _tc0_start();
} }
@@ -135,156 +110,51 @@ void cb_watch_buzzer_seq(void) {
// read note // read note
watch_buzzer_note_t note = _sequence[_seq_position]; watch_buzzer_note_t note = _sequence[_seq_position];
if (note != BUZZER_NOTE_REST) { if (note != BUZZER_NOTE_REST) {
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], _volume); watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], 25);
watch_set_buzzer_on(); watch_set_buzzer_on();
} else watch_set_buzzer_off(); } else watch_set_buzzer_off();
// set duration ticks and move to next tone // set duration ticks and move to next tone
_tone_ticks = _sequence[_seq_position + 1] - 1; _tone_ticks = _sequence[_seq_position + 1];
_seq_position += 2; _seq_position += 2;
} else { } else {
// end the sequence // end the sequence
watch_buzzer_abort_sequence(); watch_buzzer_abort_sequence();
if (_cb_finished) _cb_finished();
} }
} else _tone_ticks--; } else _tone_ticks--;
} }
void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end) {
watch_buzzer_play_raw_source_with_volume(raw_source, userdata, callback_on_end, WATCH_BUZZER_VOLUME_LOUD);
}
void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume) {
// Abort any previous sequence
watch_buzzer_abort_sequence();
if (_cb_start_global) {
_cb_start_global();
}
watch_enable_buzzer();
watch_set_buzzer_off();
_raw_source = raw_source;
_userdata = userdata;
_cb_finished = callback_on_end;
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
_seq_position = 0;
_tone_ticks = 0;
// prepare buzzer
_cb_tc0 = cb_watch_buzzer_raw_source;
// setup TC0 timer
_tc0_initialize();
// start the timer (for the 64 hz callback)
_tc0_start();
}
void cb_watch_buzzer_raw_source(void) {
// callback for reading the note sequence
uint16_t period;
uint16_t duration;
bool done;
if (_tone_ticks == 0) {
done = _raw_source(_seq_position, _userdata, &period, &duration);
if (done || duration == 0) {
// end the sequence
watch_buzzer_abort_sequence();
} else {
if (period == WATCH_BUZZER_PERIOD_REST) {
watch_set_buzzer_off();
} else {
watch_set_buzzer_period_and_duty_cycle(period, _volume);
watch_set_buzzer_on();
}
// set duration ticks and move to next tone
_tone_ticks = duration - 1;
_seq_position += 1;
}
} else {
_tone_ticks--;
}
}
void watch_buzzer_abort_sequence(void) { void watch_buzzer_abort_sequence(void) {
// ends/aborts the sequence // ends/aborts the sequence
if (!_buzzer_is_active) { if (_callback_running) _tc0_stop();
return;
}
_tc0_stop();
watch_set_buzzer_off(); watch_set_buzzer_off();
// disable standby mode for TCC
// disable TCC _tcc_write_RUNSTDBY(false);
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_start_global = cb_start;
_cb_stop_global = cb_stop;
} }
void irq_handler_tc0(void) { void irq_handler_tc0(void) {
// interrupt handler for TC0 (globally!) // interrupt handler for TC0 (globally!)
if (_cb_tc0) { cb_watch_buzzer_seq();
_cb_tc0();
}
TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
} }
void _watch_maybe_enable_tcc(void) { bool watch_is_buzzer_or_led_enabled(void){
if (!_buzzer_is_active && !_led_is_active) { return tcc_is_enabled(0);
return; }
}
inline void watch_enable_buzzer(void) {
if (!tcc_is_enabled(0)) { if (!tcc_is_enabled(0)) {
// tcc_set_run_in_standby(0, true);
_watch_enable_tcc(); _watch_enable_tcc();
// TCC should run in standby mode
_tcc_write_RUNSTDBY(true);
} }
} }
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) { void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) {
tcc_set_period(0, period, true); tcc_set_period(0, period, true);
tcc_set_cc(0, (WATCH_BUZZER_TCC_CHANNEL) % 4, period / (100 / duty), true); tcc_set_cc(0, (WATCH_BUZZER_TCC_CHANNEL) % 4, period / (100 / duty), true);
// The buzzer determines the period, which means that if the LED was active before it will flicker }
// Update the LED duty cycle to match the new period required by the buzzer.
if (_led_is_active) { void watch_disable_buzzer(void) {
_watch_set_led_duty_cycle(period, _current_led_color[0], _current_led_color[1], _current_led_color[2]); _watch_disable_tcc();
}
} }
inline void watch_set_buzzer_on(void) { inline void watch_set_buzzer_on(void) {
@@ -302,17 +172,14 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms) {
} }
void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) { void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) {
static int8_t single_note_sequence[3]; if (note == BUZZER_NOTE_REST) {
watch_set_buzzer_off();
single_note_sequence[0] = note; } else {
// 64 ticks per second for the tc0 watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25);
// Each tick is approximately 15ms watch_set_buzzer_on();
uint16_t duration = duration_ms / 15; }
if (duration > 127) duration = 127; delay_ms(duration_ms);
single_note_sequence[1] = (int8_t)duration; watch_set_buzzer_off();
single_note_sequence[2] = 0;
watch_buzzer_play_sequence_with_volume(single_note_sequence, NULL, volume);
} }
void _watch_enable_tcc(void) { void _watch_enable_tcc(void) {
@@ -353,6 +220,21 @@ void _watch_enable_tcc(void) {
tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, 0, false); tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, 0, false);
#endif #endif
// enable LED PWM pins (the LED driver assumes if the TCC is on, the pins are enabled)
HAL_GPIO_RED_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
HAL_GPIO_RED_drvstr(1);
HAL_GPIO_RED_out();
#ifdef WATCH_GREEN_TCC_CHANNEL
HAL_GPIO_GREEN_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
HAL_GPIO_GREEN_drvstr(1);
HAL_GPIO_GREEN_out();
#endif
#ifdef WATCH_BLUE_TCC_CHANNEL
HAL_GPIO_BLUE_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
HAL_GPIO_BLUE_drvstr(1);
HAL_GPIO_BLUE_out();
#endif
// Enable the TCC // Enable the TCC
tcc_enable(0); tcc_enable(0);
} }
@@ -375,45 +257,13 @@ void _watch_disable_tcc(void) {
} }
void watch_enable_leds(void) { void watch_enable_leds(void) {
_led_is_active = true; if (!tcc_is_enabled(0)) {
_watch_enable_led_pins(); _watch_enable_tcc();
_watch_maybe_enable_tcc(); }
} }
void watch_disable_leds(void) { void watch_disable_leds(void) {
_led_is_active = false; _watch_disable_tcc();
_watch_disable_led_pins();
_watch_maybe_disable_tcc();
}
void _watch_enable_led_pins(void) {
// enable LED PWM pins (the LED driver assumes if the TCC is on, the pins are enabled)
HAL_GPIO_RED_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
HAL_GPIO_RED_drvstr(1);
HAL_GPIO_RED_out();
#ifdef WATCH_GREEN_TCC_CHANNEL
HAL_GPIO_GREEN_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
HAL_GPIO_GREEN_drvstr(1);
HAL_GPIO_GREEN_out();
#endif
#ifdef WATCH_BLUE_TCC_CHANNEL
HAL_GPIO_BLUE_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
HAL_GPIO_BLUE_drvstr(1);
HAL_GPIO_BLUE_out();
#endif
}
void _watch_disable_led_pins(void) {
HAL_GPIO_RED_pmuxdis();
HAL_GPIO_RED_off();
#ifdef WATCH_GREEN_TCC_CHANNEL
HAL_GPIO_GREEN_pmuxdis();
HAL_GPIO_GREEN_off();
#endif
#ifdef WATCH_BLUE_TCC_CHANNEL
HAL_GPIO_BLUE_pmuxdis();
HAL_GPIO_BLUE_off();
#endif
} }
void watch_set_led_color(uint8_t red, uint8_t green) { void watch_set_led_color(uint8_t red, uint8_t green) {
@@ -424,7 +274,9 @@ void watch_set_led_color(uint8_t red, uint8_t green) {
#endif #endif
} }
static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue) { void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) {
if (tcc_is_enabled(0)) {
uint32_t period = tcc_get_period(0);
tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true); tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true);
#ifdef WATCH_GREEN_TCC_CHANNEL #ifdef WATCH_GREEN_TCC_CHANNEL
tcc_set_cc(0, (WATCH_GREEN_TCC_CHANNEL) % 4, ((period * (uint32_t)green * 1000ull) / 255000ull), true); tcc_set_cc(0, (WATCH_GREEN_TCC_CHANNEL) % 4, ((period * (uint32_t)green * 1000ull) / 255000ull), true);
@@ -436,23 +288,6 @@ static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t gree
#else #else
(void) blue; // silence warning (void) blue; // silence warning
#endif #endif
}
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) {
bool turning_on = (red | green | blue) != 0;
if (turning_on) {
_current_led_color[0] = red;
_current_led_color[1] = green;
_current_led_color[2] = blue;
watch_enable_leds();
uint32_t period = tcc_get_period(0);
_watch_set_led_duty_cycle(period, red, green, blue);
} else {
if (tcc_is_enabled(0)) {
_watch_set_led_duty_cycle(1, red, green, blue);
}
watch_disable_leds();
} }
} }
+1 -48
View File
@@ -277,26 +277,20 @@ inline void lis2dw_disable_fifo(void) {
#endif #endif
} }
bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data, uint32_t timeout) { bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data) {
// timeout is in terms of 1/RTC_CNT_HZ seconds (likely 128 timeouts is one second)
#ifdef I2C_SERCOM #ifdef I2C_SERCOM
uint8_t temp = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_FIFO_SAMPLE); uint8_t temp = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_FIFO_SAMPLE);
bool overrun = !!(temp & LIS2DW_FIFO_SAMPLE_OVERRUN); bool overrun = !!(temp & LIS2DW_FIFO_SAMPLE_OVERRUN);
fifo_data->count = temp & LIS2DW_FIFO_SAMPLE_COUNT; fifo_data->count = temp & LIS2DW_FIFO_SAMPLE_COUNT;
rtc_counter_t timeout_counter = watch_rtc_get_counter() + timeout;
for(int i = 0; i < fifo_data->count; i++) { for(int i = 0; i < fifo_data->count; i++) {
if (watch_rtc_get_counter() > timeout_counter) {
break;
}
fifo_data->readings[i] = lis2dw_get_raw_reading(); fifo_data->readings[i] = lis2dw_get_raw_reading();
} }
return overrun; return overrun;
#else #else
(void) fifo_data; (void) fifo_data;
(void) timeout;
return false; return false;
#endif #endif
} }
@@ -417,33 +411,6 @@ void lis2dw_configure_int2(uint8_t sources) {
#endif #endif
} }
void lis2dw12_int_notification_set(lis2dw12_lir_t val) {
#ifdef I2C_SERCOM
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3);
if (val == LIS2DW12_INT_LATCHED) {
configuration |= LIS2DW_CTRL3_VAL_LIR;
} else {
configuration &= ~LIS2DW_CTRL7_VAL_DRDY_PULSED;
}
watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3, configuration);
#else
(void)val;
#endif
}
lis2dw12_lir_t lis2dw12_int_notification_get(void) {
#ifdef I2C_SERCOM
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3);
if (configuration & LIS2DW12_INT_LATCHED) {
return LIS2DW12_INT_LATCHED;
} else {
return LIS2DW12_INT_PULSED;
}
#else
return LIS2DW12_INT_PULSED;
#endif
}
void lis2dw_enable_interrupts(void) { void lis2dw_enable_interrupts(void) {
#ifdef I2C_SERCOM #ifdef I2C_SERCOM
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7);
@@ -458,20 +425,6 @@ void lis2dw_disable_interrupts(void) {
#endif #endif
} }
void lis2dw_pulsed_drdy_interrupts(void) {
#ifdef I2C_SERCOM
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7);
watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration | LIS2DW_CTRL7_VAL_DRDY_PULSED);
#endif
}
void lis2dw_latched_drdy_interrupts(void) {
#ifdef I2C_SERCOM
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7);
watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration & ~LIS2DW_CTRL7_VAL_DRDY_PULSED);
#endif
}
lis2dw_wakeup_source_t lis2dw_get_wakeup_source() { lis2dw_wakeup_source_t lis2dw_get_wakeup_source() {
#ifdef I2C_SERCOM #ifdef I2C_SERCOM
return (lis2dw_wakeup_source_t) watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_SRC); return (lis2dw_wakeup_source_t) watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_SRC);
+1 -17
View File
@@ -92,12 +92,6 @@ typedef enum {
LIS2DW_FILTER_HIGH_PASS = 1, LIS2DW_FILTER_HIGH_PASS = 1,
} lis2dw_filter_t; } lis2dw_filter_t;
typedef enum
{
LIS2DW12_INT_PULSED = 0,
LIS2DW12_INT_LATCHED = 1,
} lis2dw12_lir_t;
typedef enum { typedef enum {
LIS2DW_RANGE_16_G = 0b11, // +/- 16g LIS2DW_RANGE_16_G = 0b11, // +/- 16g
LIS2DW_RANGE_8_G = 0b10, // +/- 8g LIS2DW_RANGE_8_G = 0b10, // +/- 8g
@@ -301,8 +295,6 @@ typedef enum {
#define LIS2DW_CTRL7_VAL_HP_REF_MODE 0b00000010 #define LIS2DW_CTRL7_VAL_HP_REF_MODE 0b00000010
#define LIS2DW_CTRL7_VAL_LPASS_ON6D 0b00000001 #define LIS2DW_CTRL7_VAL_LPASS_ON6D 0b00000001
#define LIS2DW_FIFO_TIMEOUT 100 // timeout is in terms of 1/RTC_CNT_HZ seconds (likely 128 timeouts is one second)
bool lis2dw_begin(void); bool lis2dw_begin(void);
uint8_t lis2dw_get_device_id(void); uint8_t lis2dw_get_device_id(void);
@@ -347,7 +339,7 @@ void lis2dw_enable_fifo(void);
void lis2dw_disable_fifo(void); void lis2dw_disable_fifo(void);
bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data, uint32_t timeout); bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data);
void lis2dw_clear_fifo(void); void lis2dw_clear_fifo(void);
@@ -375,10 +367,6 @@ void lis2dw_configure_tap_threshold(uint8_t threshold_x, uint8_t threshold_y, ui
void lis2dw_configure_tap_duration(uint8_t latency, uint8_t quiet, uint8_t shock); void lis2dw_configure_tap_duration(uint8_t latency, uint8_t quiet, uint8_t shock);
void lis2dw12_int_notification_set(lis2dw12_lir_t val);
lis2dw12_lir_t lis2dw12_int_notification_get(void);
void lis2dw_configure_int1(uint8_t sources); void lis2dw_configure_int1(uint8_t sources);
void lis2dw_configure_int2(uint8_t sources); void lis2dw_configure_int2(uint8_t sources);
@@ -387,10 +375,6 @@ void lis2dw_enable_interrupts(void);
void lis2dw_disable_interrupts(void); void lis2dw_disable_interrupts(void);
void lis2dw_pulsed_drdy_interrupts(void);
void lis2dw_latched_drdy_interrupts(void);
lis2dw_interrupt_source_t lis2dw_get_interrupt_source(void); lis2dw_interrupt_source_t lis2dw_get_interrupt_source(void);
lis2dw_wakeup_source_t lis2dw_get_wakeup_source(void); lis2dw_wakeup_source_t lis2dw_get_wakeup_source(void);
-98
View File
@@ -1,98 +0,0 @@
////< @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);
/** @} */
+22 -74
View File
@@ -27,7 +27,7 @@
////< @file watch_rtc.h ////< @file watch_rtc.h
#include "watch.h" #include "watch.h"
#include "rtc32.h" #include "rtc.h"
/** @addtogroup rtc Real-Time Clock /** @addtogroup rtc Real-Time Clock
* @brief This section covers functions related to the SAM L22's real-time clock peripheral, including * @brief This section covers functions related to the SAM L22's real-time clock peripheral, including
@@ -42,20 +42,17 @@
extern watch_cb_t btn_alarm_callback; extern watch_cb_t btn_alarm_callback;
extern watch_cb_t a2_callback; extern watch_cb_t a2_callback;
extern watch_cb_t a4_callback; extern watch_cb_t a4_callback;
extern watch_cb_t comp_callback;
#define WATCH_RTC_REFERENCE_YEAR (2020) #define WATCH_RTC_REFERENCE_YEAR (2020)
#define watch_date_time_t rtc_date_time_t #define watch_date_time_t rtc_date_time_t
typedef rtc_counter_t watch_counter_t;
typedef uint32_t unix_timestamp_t;
/** @brief Called by main.c to check if the RTC is enabled. /** @brief Called by main.c to check if the RTC is enabled.
* You may call this function, but outside of app_init, it should always return true. * You may call this function, but outside of app_init, it should always return true.
*/ */
bool _watch_rtc_is_enabled(void); bool _watch_rtc_is_enabled(void);
/** @brief Sets the date and time. Calls watch_rtc_set_unix_time internally. /** @brief Sets the date and time.
* @param date_time The date and time you wish to set, with a year value from 0-63 representing 2020-2083. * @param date_time The date and time you wish to set, with a year value from 0-63 representing 2020-2083.
* @note The SAM L22 stores the year as six bits representing a value from 0 to 63. It treats this as a year * @note The SAM L22 stores the year as six bits representing a value from 0 to 63. It treats this as a year
* offset from a reference year, which must be a leap year. Since 2020 was a leap year, and it allows * offset from a reference year, which must be a leap year. Since 2020 was a leap year, and it allows
@@ -65,7 +62,7 @@ bool _watch_rtc_is_enabled(void);
*/ */
void watch_rtc_set_date_time(rtc_date_time_t date_time); void watch_rtc_set_date_time(rtc_date_time_t date_time);
/** @brief Returns the date and time. Calls watch_rtc_get_unix_time internally. /** @brief Returns the date and time.
* @return A rtc_date_time_t with the current date and time, with a year value from 0-63 representing 2020-2083. * @return A rtc_date_time_t with the current date and time, with a year value from 0-63 representing 2020-2083.
* @see watch_rtc_set_date_time for notes about how the year is stored. * @see watch_rtc_set_date_time for notes about how the year is stored.
*/ */
@@ -76,79 +73,26 @@ rtc_date_time_t watch_rtc_get_date_time(void);
*/ */
rtc_date_time_t watch_get_init_date_time(void); rtc_date_time_t watch_get_init_date_time(void);
/** @brief Set the current UTC date and time using a unix timestamp /** @brief Registers an alarm callback that will be called when the RTC time matches the target time, as masked
*/ * by the provided mask.
void watch_rtc_set_unix_time(unix_timestamp_t unix_time); * @param callback The function you wish to have called when the alarm fires. If this value is NULL, the alarm
/** @brief Get the current UTC date and time using a unix timestamp
*/
unix_timestamp_t watch_rtc_get_unix_time(void);
/** @brief Get the current value of the internal hardware counter
* @details The counter starts at 0 and it increases at a 128Hz rate until it overflows and starts over.
* We never manually set the counter. Doing so allows us to calculate absolute elapsed and more.
* When the user sets the time, what is modified is the reference time (i.e. the date and time when
* the counter is 0).
*/
rtc_counter_t watch_rtc_get_counter(void);
/** @brief Get the RTC counter frequency.
*/
uint32_t watch_rtc_get_frequency(void);
/** @brief Get how many counter ticks are in one minute.
*/
uint32_t watch_rtc_get_ticks_per_minute(void);
/** @brief Registers a callback that will be called when the RTC counter matches the target counter.
* @param callback The function you wish to have called when the target counter is reached. If this value is NULL, the comp
* interrupt will still be enabled, but no callback function will be called. * interrupt will still be enabled, but no callback function will be called.
* @param counter The time that you wish to match. The date is currently ignored. * @param alarm_time 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. * @param mask One of the values in rtc_alarm_match_t indicating which values to check.
* @details The hardware RTC provides us with single interrupt that fires when the RTC counter matches a target counter COMP0. * @details The alarm interrupt is a versatile tool for scheduling events in the future, especially since it can
* With a little bit of logic, we can provide multiple active compare callbacks. Every time a comp callback is * wake the device from all sleep modes. The key to its versatility is the mask parameter.
* registered/disabled/fired we iterate over all the active comp callbacks and set the hardware COMP0 counter * Suppose we set an alarm for midnight, 00:00:00.
* to the next occurring one. * * if mask is ALARM_MATCH_SS, the alarm will fire every minute when the clock ticks to seconds == 0.
* With this very simple API, movement can implement one-shot timers to turn off the led and determine button longpresses * * with ALARM_MATCH_MMSS, the alarm will once an hour, at the top of each hour.
* as well as the inactivity timeouts for resigning and sleeping, as well as emulating the top of the minute alarm. * * 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.
*/ */
void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index); void watch_rtc_register_alarm_callback(watch_cb_t callback, rtc_date_time_t alarm_time, rtc_alarm_match_t mask);
/** @brief Just like watch_rtc_register_comp_callback but doesn't actually schedule the callback
*
* Useful if you need register multiple callbacks at once, avoids multiple calls to the expensive watch_rtc_schedule_next_comp:
* Usage:
* watch_rtc_register_comp_callback_no_schedule(cb0, counter0, index0);
* watch_rtc_register_comp_callback_no_schedule(cb1, counter1, index1);
* watch_rtc_schedule_next_comp();
*/
void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index);
/** @brief Disables the specified comp callback.
*/
void watch_rtc_disable_comp_callback(uint8_t index);
/** @brief Just like watch_rtc_disable_comp_callback but doesn't actually schedule the callback
*
* Useful if you need disable multiple callbacks at once, avoids multiple calls to the expensive watch_rtc_schedule_next_comp:
* Usage:
* watch_rtc_disable_comp_callback_no_schedule(index0);
* watch_rtc_disable_comp_callback_no_schedule(index1);
* watch_rtc_schedule_next_comp();
*/
/** @brief Disables the specified comp callback.
*/
void watch_rtc_disable_comp_callback_no_schedule(uint8_t index);
/** @brief Determines the first comp callback that should fire and schedule it with the RTC
*
* You would never need to call this manually, unless you used the 'no_schedule' functions above.
*/
void watch_rtc_schedule_next_comp(void);
/** @brief Disables the alarm callback. /** @brief Disables the alarm callback.
*/ */
// void watch_rtc_disable_alarm_callback(void); void watch_rtc_disable_alarm_callback(void);
/** @brief Registers a "tick" callback that will be called once per second. /** @brief Registers a "tick" callback that will be called once per second.
* @param callback The function you wish to have called when the clock ticks. If you pass in NULL, the tick * @param callback The function you wish to have called when the clock ticks. If you pass in NULL, the tick
@@ -173,6 +117,10 @@ void watch_rtc_disable_tick_callback(void);
* tick at 16 or 32 Hz to update the screen more quickly. Just remember that the more frequent the tick, the more * tick at 16 or 32 Hz to update the screen more quickly. Just remember that the more frequent the tick, the more
* power your app will consume. Ideally you should enable the fast tick only when the user requires it (i.e. in * power your app will consume. Ideally you should enable the fast tick only when the user requires it (i.e. in
* response to an input event), and move back to the slow tick after some time. * response to an input event), and move back to the slow tick after some time.
*
* Also note that the RTC peripheral does not have sub-second resolution, so even if you set a 2 or 4 Hz interval,
* the system will not have any way of telling you where you are within a given second; watch_rtc_get_date_time
* will return the exact same timestamp until the second ticks over.
*/ */
void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency); void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency);
+32 -67
View File
@@ -126,9 +126,12 @@ typedef enum {
BUZZER_NOTE_REST ///< no sound BUZZER_NOTE_REST ///< no sound
} watch_buzzer_note_t; } watch_buzzer_note_t;
#define WATCH_BUZZER_PERIOD_REST 0 /** @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
typedef bool (*watch_buzzer_raw_source_t)(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration); * 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);
/** @addtogroup tcc Buzzer and LED Control (via the TCC peripheral) /** @addtogroup tcc Buzzer and LED Control (via the TCC peripheral)
* @brief This section covers functions related to Timer Counter for Control peripheral, which drives the piezo buzzer * @brief This section covers functions related to Timer Counter for Control peripheral, which drives the piezo buzzer
@@ -148,13 +151,15 @@ void watch_enable_buzzer(void);
*/ */
void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty); void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty);
/** @brief Disables the TCC peripheral that drives the buzzer (if LED not active). /** @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.
*/ */
void watch_disable_buzzer(void); void watch_disable_buzzer(void);
/** @brief Turns the buzzer output on. It will emit a continuous sound at the given frequency. /** @brief Turns the buzzer output on. It will emit a continuous sound at the given frequency.
* @note The TCC peripheral that drives the buzzer does run in standby mode; if you wish for buzzer * @note The TCC peripheral that drives the buzzer does not run in standby mode; if you wish for buzzer
* output to continue, you don't need to prevent your app from going to sleep. * output to continue, you should prevent your app from going to sleep.
*/ */
void watch_set_buzzer_on(void); void watch_set_buzzer_on(void);
@@ -165,6 +170,8 @@ void watch_set_buzzer_off(void);
/** @brief Plays the given note for a set duration at the loudest possible volume. /** @brief Plays the given note for a set duration at the loudest possible volume.
* @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration. * @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration.
* @param duration_ms The duration of the note. * @param duration_ms The duration of the note.
* @note Note that this will block your UI for the duration of the note's play time, and it will
* after this call, the buzzer period will be set to the period of this note.
*/ */
void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms); void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms);
@@ -172,6 +179,8 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms);
* @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration. * @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration.
* @param duration_ms The duration of the note. * @param duration_ms The duration of the note.
* @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD * @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD
* @note This will block your UI for the duration of the note's play time, and after this call, the
* buzzer will stop sounding, but the TCC period will remain set to the period of this note.
*/ */
void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume); void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume);
@@ -193,69 +202,10 @@ extern const uint16_t NotePeriods[108];
*/ */
void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)); void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void));
/** @brief Plays the given sequence of notes in a non-blocking way.
* @param note_sequence A pointer to the sequence of buzzer note & duration tuples, ending with a zero. A simple
* RLE logic is implemented: a negative number instead of a buzzer note means that the sequence
* is rewound by the given number of notes. The byte following a negative number determines the number
* of loops. I.e. if you want to repeat the last three notes of the sequence one time, you should provide
* the tuple -3, 1. The repeated notes must not contain any other repeat markers, or you will end up with
* an eternal loop.
* @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing.
* @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD
*/
void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume);
/** @brief Plays the given raw buzzer source function in a non-blocking way.
*
* @details This function plays audio data generated by a raw source callback function,
* allowing for precise control over buzzer timing and frequency. The raw source
* function is called repeatedly to generate audio samples, each containing a
* period and duration for the buzzer tone.
* Useful for applications such as chirpy, so that they won't need to allocate a
* long note sequence, and we will also take care of all the timing logic.
*
* @param raw_source Pointer to the callback function that generates raw buzzer data.
* The function should take a position parameter and return true if
* more data is available, false if end of sequence is reached.
* Parameters:
* - position: Current position in the audio sequence (0-based)
* - userdata: User-provided data passed through to the callback
* - period: Pointer to store the period (in microseconds) for the tone
* - duration: Pointer to store the duration (in microseconds) for the tone
* @param userdata Pointer to user data that will be passed to the raw_source callback
* @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing.
*/
void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end);
/** @brief Plays the given raw buzzer source function in a non-blocking way.
*
* @details This function plays audio data generated by a raw source callback function,
* allowing for precise control over buzzer timing and frequency. The raw source
* function is called repeatedly to generate audio samples, each containing a
* period and duration for the buzzer tone.
* Useful for applications such as chirpy, so that they won't need to allocate a
* long note sequence, and we will also take care of all the timing logic.
*
* @param raw_source Pointer to the callback function that generates raw buzzer data.
* The function should take a position parameter and return true if
* more data is available, false if end of sequence is reached.
* Parameters:
* - position: Current position in the audio sequence (0-based)
* - userdata: User-provided data passed through to the callback
* - period: Pointer to store the period (in microseconds) for the tone
* - duration: Pointer to store the duration (in microseconds) for the tone
* @param userdata Pointer to user data that will be passed to the raw_source callback
* @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing.
* @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD
*/
void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume);
/** @brief Aborts a playing sequence. /** @brief Aborts a playing sequence.
*/ */
void watch_buzzer_abort_sequence(void); void watch_buzzer_abort_sequence(void);
void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop);
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__
void irq_handler_tc0(void); void irq_handler_tc0(void);
#endif #endif
@@ -273,17 +223,26 @@ void irq_handler_tc0(void);
* so that watch_set_led_red sets the red LED, and watch_set_led_green sets the blue one. * so that watch_set_led_red sets the red LED, and watch_set_led_green sets the blue one.
*/ */
/// @{ /// @{
/** @brief Enables the TCC peripheral, which drives the LEDs. /** @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.
*/ */
void watch_enable_leds(void); void watch_enable_leds(void);
/** @brief Disables the TCC peripheral that drives the LEDs (if buzzer not active). /** @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.
*/ */
void watch_disable_leds(void); void watch_disable_leds(void);
/** @brief Sets the LED to a custom color by modulating each output's duty cycle. /** @brief Sets the LED to a custom color by modulating each output's duty cycle.
* @param red The red value from 0-255. * @param red The red value from 0-255.
* @param green The green value from 0-255. If your watch has a red/blue LED, this will be the blue value. * @param green The green value from 0-255. If your watch has a red/blue LED, this will be the blue value.
* @note If you are displaying a custom color, you will need to prevent your app from going to sleep
* while the LED is on; otherwise, the color will not display correctly. You can do this by
* returning false in your app_loop method.
*/ */
void watch_set_led_color(uint8_t red, uint8_t green); void watch_set_led_color(uint8_t red, uint8_t green);
@@ -291,6 +250,9 @@ void watch_set_led_color(uint8_t red, uint8_t green);
* @param red The red value from 0-255. * @param red The red value from 0-255.
* @param green The green value from 0-255. * @param green The green value from 0-255.
* @param blue The blue value from 0-255. * @param blue The blue value from 0-255.
* @note If you are displaying a custom color, you will need to prevent your app from going to sleep
* while the LED is on; otherwise, the color will not display correctly. You can do this by
* returning false in your app_loop method.
*/ */
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue); void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue);
@@ -315,6 +277,9 @@ void watch_set_led_yellow(void);
/** @brief Turns both the red and the green LEDs off. */ /** @brief Turns both the red and the green LEDs off. */
void watch_set_led_off(void); void watch_set_led_off(void);
/** @brief Disables the TCC peripheral. Should only be called internally. */
void _watch_disable_tcc(void);
/// @brief An array of periods for all the notes on a piano, corresponding to the names in watch_buzzer_note_t. /// @brief An array of periods for all the notes on a piano, corresponding to the names in watch_buzzer_note_t.
extern const uint16_t NotePeriods[108]; extern const uint16_t NotePeriods[108];
+2 -6
View File
@@ -28,12 +28,12 @@
#include "zones.h" #include "zones.h"
const char * watch_utility_get_weekday(watch_date_time_t date_time) { const char * watch_utility_get_weekday(watch_date_time_t date_time) {
static const char weekdays[7][3] = {"LU", "MA", "MI", "JU", "VI", "SA", "do"}; static const char weekdays[7][3] = {"MO", "TU", "WE", "TH", "FR", "SA", "SU"};
return weekdays[watch_utility_get_iso8601_weekday_number(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR, date_time.unit.month, date_time.unit.day) - 1]; return weekdays[watch_utility_get_iso8601_weekday_number(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR, date_time.unit.month, date_time.unit.day) - 1];
} }
const char * watch_utility_get_long_weekday(watch_date_time_t date_time) { const char * watch_utility_get_long_weekday(watch_date_time_t date_time) {
static const char weekdays[7][4] = {"LUN", "MAR", "MIE", "JUE", "VIE", "SAB", "DOM"}; static const char weekdays[7][4] = {"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"};
return weekdays[watch_utility_get_iso8601_weekday_number(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR, date_time.unit.month, date_time.unit.day) - 1]; return weekdays[watch_utility_get_iso8601_weekday_number(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR, date_time.unit.month, date_time.unit.day) - 1];
} }
@@ -278,10 +278,6 @@ watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_ti
return watch_utility_date_time_from_unix_time(timestamp, destination_utc_offset); return watch_utility_date_time_from_unix_time(timestamp, destination_utc_offset);
} }
uint32_t watch_utility_unix_time_convert_zone(uint32_t timestamp, uint32_t origin_utc_offset, uint32_t destination_utc_offset) {
return timestamp - origin_utc_offset + destination_utc_offset;
}
watch_duration_t watch_utility_seconds_to_duration(uint32_t seconds) { watch_duration_t watch_utility_seconds_to_duration(uint32_t seconds) {
watch_duration_t retval; watch_duration_t retval;
@@ -144,16 +144,6 @@ bool watch_utility_convert_to_12_hour(watch_date_time_t *date_time);
*/ */
watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_time, uint32_t origin_utc_offset, uint32_t destination_utc_offset); watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_time, uint32_t origin_utc_offset, uint32_t destination_utc_offset);
/** @brief Converts a unix time from a given time zone to another time zone.
* @param timestamp The unix time that you wish to convert
* @param origin_utc_offset The number of seconds from UTC in the origin time zone
* @param destination_utc_offset The number of seconds from UTC in the destination time zone
* @return A unix time for the given UNIX timestamp and UTC offset.
* @note Adapted from MIT-licensed code from musl, Copyright © 2005-2014 Rich Felker, et al.:
* https://github.com/esmil/musl/blob/1cc81f5cb0df2b66a795ff0c26d7bbc4d16e13c6/src/time/__secs_to_tm.c
*/
uint32_t watch_utility_unix_time_convert_zone(uint32_t timestamp, uint32_t origin_utc_offset, uint32_t destination_utc_offset);
/** @brief Returns a temperature in degrees Celsius for a given thermistor voltage divider circuit. /** @brief Returns a temperature in degrees Celsius for a given thermistor voltage divider circuit.
* @param value The raw analog reading from the thermistor pin (0-65535) * @param value The raw analog reading from the thermistor pin (0-65535)
* @param highside True if the thermistor is connected to VCC and the series resistor is connected * @param highside True if the thermistor is connected to VCC and the series resistor is connected
+4
View File
@@ -1,5 +1,9 @@
#include "watch.h" #include "watch.h"
bool watch_is_buzzer_or_led_enabled(void) {
return false;
}
bool watch_is_usb_enabled(void) { bool watch_is_usb_enabled(void) {
return true; return true;
} }
@@ -51,6 +51,8 @@ int _gettimeofday(struct timeval *tv, void *tzvp) {
return 0; return 0;
} }
void _watch_disable_tcc(void) {}
void _watch_enable_usb(void) {} void _watch_enable_usb(void) {}
void watch_disable_TRNG() {} void watch_disable_TRNG() {}
+115 -250
View File
@@ -21,8 +21,6 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
#include <limits.h>
#include <stdbool.h>
#include "watch_rtc.h" #include "watch_rtc.h"
#include "watch_main_loop.h" #include "watch_main_loop.h"
@@ -31,29 +29,8 @@
#include <emscripten.h> #include <emscripten.h>
#include <emscripten/html5.h> #include <emscripten/html5.h>
static const uint32_t RTC_CNT_HZ = 128;
static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1;
static const uint32_t RTC_CNT_DIV = 7;
static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60;
static bool rtc_enabled;
static uint32_t counter;
static uint32_t reference_timestamp;
static double next_tick_time;
#define WATCH_RTC_N_COMP_CB 8
typedef struct {
volatile uint32_t counter;
volatile watch_cb_t callback;
volatile bool enabled;
} comp_cb_t;
static double time_offset = 0; static double time_offset = 0;
watch_cb_t tick_callbacks[8]; static long tick_callbacks[8] = { -1, -1, -1, -1, -1, -1, -1, -1 };
comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB];
static uint32_t scheduled_comp_counter;
static long alarm_interval_id = -1; static long alarm_interval_id = -1;
static long alarm_timeout_id = -1; static long alarm_timeout_id = -1;
@@ -63,73 +40,41 @@ watch_cb_t btn_alarm_callback;
watch_cb_t a2_callback; watch_cb_t a2_callback;
watch_cb_t a4_callback; watch_cb_t a4_callback;
static void _watch_increase_counter(void *userData);
static void _watch_process_periodic_callbacks(void);
static void _watch_process_comp_callbacks(void);
bool _watch_rtc_is_enabled(void) { bool _watch_rtc_is_enabled(void) {
return rtc_enabled; return true;
} }
void _watch_rtc_init(void) { void _watch_rtc_init(void) {
for (uint8_t index = 0; index < 8; ++index) { #if EMSCRIPTEN
tick_callbacks[index] = NULL; // Shifts the timezone so our local time is converted to UTC and set
}
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
comp_callbacks[index].counter = 0;
comp_callbacks[index].callback = NULL;
comp_callbacks[index].enabled = false;
}
scheduled_comp_counter = 0;
counter = 0;
rtc_enabled = false;
watch_rtc_set_date_time(watch_get_init_date_time());
watch_rtc_enable(true);
}
void watch_rtc_set_date_time(rtc_date_time_t date_time) {
watch_rtc_set_unix_time(watch_utility_date_time_to_unix_time(date_time, 0));
}
rtc_date_time_t watch_rtc_get_date_time(void) {
return watch_utility_date_time_from_unix_time(watch_rtc_get_unix_time(), 0);
}
void watch_rtc_set_unix_time(unix_timestamp_t unix_time) {
// unix_time = time_backup + counter / RTC_CNT_HZ - 0.5
rtc_counter_t counter = watch_rtc_get_counter();
reference_timestamp = unix_time - (counter >> RTC_CNT_DIV) - ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) + 1;
}
unix_timestamp_t watch_rtc_get_unix_time(void) {
// unix_time = time_backup + counter / RTC_CNT_HZ - 0.5
rtc_counter_t counter = watch_rtc_get_counter();
return reference_timestamp + (counter >> RTC_CNT_DIV) + ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) - 1;
}
rtc_counter_t watch_rtc_get_counter(void) {
return counter;
}
uint32_t watch_rtc_get_frequency(void) {
return RTC_CNT_HZ;
}
uint32_t watch_rtc_get_ticks_per_minute(void) {
return RTC_CNT_TICKS_PER_MINUTE;
}
rtc_date_time_t watch_get_init_date_time(void) {
rtc_date_time_t date_time = {0};
int32_t time_zone_offset = EM_ASM_INT({ int32_t time_zone_offset = EM_ASM_INT({
return new Date().getTimezoneOffset() * 60 * 1000; // ms return -new Date().getTimezoneOffset() * 60;
}); });
#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));
}
date_time.reg = EM_ASM_INT({ 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({
const date = new Date(Date.now() + $0); const date = new Date(Date.now() + $0);
return date.getSeconds() | return date.getSeconds() |
(date.getMinutes() << 6) | (date.getMinutes() << 6) |
@@ -137,16 +82,27 @@ rtc_date_time_t watch_get_init_date_time(void) {
(date.getDate() << 17) | (date.getDate() << 17) |
((date.getMonth() + 1) << 22) | ((date.getMonth() + 1) << 22) |
((date.getFullYear() - 2020) << 26); ((date.getFullYear() - 2020) << 26);
}, time_zone_offset); }, time_offset);
return retval;
}
rtc_date_time_t watch_get_init_date_time(void) {
rtc_date_time_t date_time = {0};
#ifdef BUILD_YEAR #ifdef BUILD_YEAR
date_time.unit.year = BUILD_YEAR; date_time.unit.year = BUILD_YEAR;
#else
date_time.unit.year = 5;
#endif #endif
#ifdef BUILD_MONTH #ifdef BUILD_MONTH
date_time.unit.month = BUILD_MONTH; date_time.unit.month = BUILD_MONTH;
#else
date_time.unit.month = 1;
#endif #endif
#ifdef BUILD_DAY #ifdef BUILD_DAY
date_time.unit.day = BUILD_DAY; date_time.unit.day = BUILD_DAY;
#else
date_time.unit.day = 1;
#endif #endif
#ifdef BUILD_HOUR #ifdef BUILD_HOUR
date_time.unit.hour = BUILD_HOUR; date_time.unit.hour = BUILD_HOUR;
@@ -166,94 +122,10 @@ void watch_rtc_disable_tick_callback(void) {
watch_rtc_disable_periodic_callback(1); watch_rtc_disable_periodic_callback(1);
} }
static void _watch_schedule_next_tick(void) { static void watch_invoke_periodic_callback(void *userData) {
if (!rtc_enabled) return; watch_cb_t callback = userData;
callback();
double now = EM_ASM_DOUBLE({ return performance.now(); });
// Target interval in ms
double ms = 1000.0 / (double)RTC_CNT_HZ;
// Schedule next tick, correcting for drift
next_tick_time += ms;
double delay = next_tick_time - now;
// If we're behind, reset timing
if (delay < 0) {
next_tick_time = now + ms;
delay = ms;
}
emscripten_async_call(_watch_increase_counter, NULL, delay);
}
static void _watch_increase_counter(void *userData) {
(void) userData;
counter += 1;
// Fire the periodic callbacks that match this counter
_watch_process_periodic_callbacks();
// Fire the comp callbacks that match this counter
_watch_process_comp_callbacks();
resume_main_loop(); resume_main_loop();
// Schedule the next tick with drift correction
_watch_schedule_next_tick();
}
static void _watch_process_periodic_callbacks(void) {
/* It looks weird but it follows the way the hardware triggers periodic interrupts.
* For 128hz counter periodic interrupts fire at these tick values:
* 1Hz: 64
* 2Hz: 32, 96
* 4Hz: 16, 48, 80, 112
* 8Hz: 8, 24, 40, 56, 72, 88, 104, 120
* 16Hz: 4, 12, 20, ..., 124
* 32Hz: 2, 6, 10, ..., 126
* 64Hz: 1, 3, 5, ..., 127
* 128Hz: 0, 1, 2, ..., 127
*
* Which means that only one periodic interrupt can fire for a given counter value
* (except 128Hz which can always fire)
*/
uint32_t freq = watch_rtc_get_frequency();
uint32_t subsecond_mask = freq - 1;
uint32_t subseconds = counter & subsecond_mask;
// Find the firs non-zero bit in the counter, which can be used to determine the appropriate period (see table above).
uint8_t per_n = 0;
for (uint8_t i = 0; i < 7; i++) {
if (subseconds & (1 << i)) {
per_n = i + 1;
break;
}
}
if (tick_callbacks[per_n]) {
tick_callbacks[per_n]();
}
// 128Hz is always a match
if (per_n != 0 && tick_callbacks[0]) {
tick_callbacks[0]();
}
}
static void _watch_process_comp_callbacks(void) {
// In hardware the interrupt fires one tick after the matching counter
if (counter == (scheduled_comp_counter + 1)) {
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
if (comp_callbacks[index].enabled && scheduled_comp_counter == comp_callbacks[index].counter) {
comp_callbacks[index].enabled = false;
comp_callbacks[index].callback();
}
}
watch_rtc_schedule_next_comp();
}
} }
void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency) { void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency) {
@@ -266,19 +138,26 @@ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency
// 0x01 (1 Hz) will have 7 leading zeros for PER7. 0xF0 (128 Hz) will have no leading zeroes for PER0. // 0x01 (1 Hz) will have 7 leading zeros for PER7. 0xF0 (128 Hz) will have no leading zeroes for PER0.
uint8_t per_n = __builtin_clz(tmp); uint8_t per_n = __builtin_clz(tmp);
tick_callbacks[per_n] = callback; 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);
} }
void watch_rtc_disable_periodic_callback(uint8_t frequency) { void watch_rtc_disable_periodic_callback(uint8_t frequency) {
if (__builtin_popcount(frequency) != 1) return; if (__builtin_popcount(frequency) != 1) return;
uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24); uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24);
tick_callbacks[per_n] = NULL; if (tick_callbacks[per_n] != -1) {
emscripten_clear_interval(tick_callbacks[per_n]);
tick_callbacks[per_n] = -1;
}
} }
void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) { void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) {
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
if (tick_callbacks[i] && (mask & (1 << i)) != 0) { if (tick_callbacks[i] != -1 && (mask & (1 << i)) != 0) {
tick_callbacks[i] = NULL; emscripten_clear_interval(tick_callbacks[i]);
tick_callbacks[i] = -1;
} }
} }
} }
@@ -287,95 +166,81 @@ void watch_rtc_disable_all_periodic_callbacks(void) {
watch_rtc_disable_matching_periodic_callbacks(0xFF); watch_rtc_disable_matching_periodic_callbacks(0xFF);
} }
void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { static void watch_invoke_alarm_interval_callback(void *userData) {
if (index >= WATCH_RTC_N_COMP_CB) { if (alarm_callback) alarm_callback();
return;
}
comp_callbacks[index].counter = counter;
comp_callbacks[index].callback = callback;
comp_callbacks[index].enabled = true;
watch_rtc_schedule_next_comp();
} }
void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { static void watch_invoke_alarm_callback(void *userData) {
if (index >= WATCH_RTC_N_COMP_CB) { if (alarm_callback) alarm_callback();
return; alarm_interval_id = emscripten_set_interval(watch_invoke_alarm_interval_callback, alarm_interval, NULL);
}
comp_callbacks[index].counter = counter;
comp_callbacks[index].callback = callback;
comp_callbacks[index].enabled = true;
} }
void watch_rtc_disable_comp_callback(uint8_t index) { void watch_rtc_register_alarm_callback(watch_cb_t callback, watch_date_time_t alarm_time, rtc_alarm_match_t mask) {
if (index >= WATCH_RTC_N_COMP_CB) { watch_rtc_disable_alarm_callback();
switch (mask) {
case ALARM_MATCH_DISABLED:
return; 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;
} }
comp_callbacks[index].enabled = false; double timeout = EM_ASM_DOUBLE({
const now = Date.now();
const date = new Date(now);
watch_rtc_schedule_next_comp(); const hour = ($0 >> 12) & 0x1f;
} const minute = ($0 >> 6) & 0x3f;
const second = $0 & 0x3f;
void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { if ($1 == 1) { // SS
if (index >= WATCH_RTC_N_COMP_CB) { if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1);
return; date.setSeconds(second);
} } else if ($1 == 2) { // MMSS
if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1);
comp_callbacks[index].enabled = false; if (minute < date.getMinutes()) date.setHours(date.getHours() + 1);
} date.setMinutes(minute, second);
} else if ($1 == 3) { // HHMMSS
void watch_rtc_schedule_next_comp(void) { if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1);
rtc_counter_t curr_counter = watch_rtc_get_counter(); if (minute < date.getMinutes()) date.setHours(date.getHours() + 1);
// If there is already a pending comp interrupt for this very tick, let it fire if (hour < date.getHours()) date.setDate(date.getDate() + 1);
// And this function will be called again as soon as the interrupt fires. date.setHours(hour, minute, second);
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 { } else {
scheduled_comp_counter = curr_counter - 2; 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);
}
void watch_rtc_disable_alarm_callback(void) {
alarm_callback = NULL;
alarm_interval = 0;
if (alarm_timeout_id != -1) {
emscripten_clear_timeout(alarm_timeout_id);
alarm_timeout_id = -1;
}
if (alarm_interval_id != -1) {
emscripten_clear_interval(alarm_interval_id);
alarm_interval_id = -1;
} }
} }
void watch_rtc_enable(bool en) void watch_rtc_enable(bool en)
{ {
// Nothing to do cases //Not simulated
if ((en && rtc_enabled) || (!en && !rtc_enabled)) {
return;
}
if (en) {
// Use drift-correcting timer instead of fixed setInterval
rtc_enabled = true;
next_tick_time = EM_ASM_DOUBLE({ return performance.now(); });
_watch_schedule_next_tick();
} else {
rtc_enabled = false;
}
} }
void watch_rtc_freqcorr_write(int16_t value, int16_t sign) void watch_rtc_freqcorr_write(int16_t value, int16_t sign)
+25 -121
View File
@@ -28,23 +28,18 @@
#include <emscripten.h> #include <emscripten.h>
#include <emscripten/html5.h> #include <emscripten/html5.h>
static volatile bool buzzer_enabled = false; static bool buzzer_enabled = false;
static uint32_t buzzer_period; static uint32_t buzzer_period;
void cb_watch_buzzer_seq(void *userData); void cb_watch_buzzer_seq(void *userData);
void cb_watch_buzzer_raw_source(void *userData);
static uint16_t _seq_position; static uint16_t _seq_position;
static int8_t _tone_ticks, _repeat_counter; static int8_t _tone_ticks, _repeat_counter;
static volatile long _em_interval_id = 0; static long _em_interval_id = 0;
static int8_t *_sequence; static int8_t *_sequence;
static watch_buzzer_raw_source_t _raw_source;
static void* _userdata;
static uint8_t _volume;
static void (*_cb_finished)(void); static void (*_cb_finished)(void);
static watch_cb_t _cb_start_global = NULL;
static watch_cb_t _cb_stop_global = NULL; void _watch_enable_tcc(void) {}
static volatile bool _buzzer_is_active = false;
static inline void _em_interval_stop() { static inline void _em_interval_stop() {
emscripten_clear_interval(_em_interval_id); emscripten_clear_interval(_em_interval_id);
@@ -52,28 +47,15 @@ static inline void _em_interval_stop() {
} }
void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) { void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) {
watch_buzzer_play_sequence_with_volume(note_sequence, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); if (_em_interval_id) _em_interval_stop();
}
void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) {
watch_buzzer_abort_sequence();
// prepare buzzer
watch_enable_buzzer();
watch_set_buzzer_off(); watch_set_buzzer_off();
_buzzer_is_active = true;
if (_cb_start_global) {
_cb_start_global();
}
_sequence = note_sequence; _sequence = note_sequence;
_cb_finished = callback_on_end; _cb_finished = callback_on_end;
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
_seq_position = 0; _seq_position = 0;
_tone_ticks = 0; _tone_ticks = 0;
_repeat_counter = -1; _repeat_counter = -1;
// prepare buzzer
watch_enable_buzzer();
// initiate 64 hz callback // initiate 64 hz callback
_em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL); _em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL);
} }
@@ -106,115 +88,32 @@ void cb_watch_buzzer_seq(void *userData) {
if (note == BUZZER_NOTE_REST) { if (note == BUZZER_NOTE_REST) {
watch_set_buzzer_off(); watch_set_buzzer_off();
} else { } else {
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], _volume); watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], 25);
watch_set_buzzer_on(); watch_set_buzzer_on();
} }
// set duration ticks and move to next tone // set duration ticks and move to next tone
_tone_ticks = _sequence[_seq_position + 1] - 1; _tone_ticks = _sequence[_seq_position + 1];
_seq_position += 2; _seq_position += 2;
} else { } else {
// end the sequence // end the sequence
watch_buzzer_abort_sequence(); watch_buzzer_abort_sequence();
if (_cb_finished) _cb_finished();
} }
} else _tone_ticks--; } else _tone_ticks--;
} }
void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end) {
watch_buzzer_play_raw_source_with_volume(raw_source, userdata, callback_on_end, WATCH_BUZZER_VOLUME_LOUD);
}
void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume) {
watch_buzzer_abort_sequence();
// prepare buzzer
watch_enable_buzzer();
watch_set_buzzer_off();
_buzzer_is_active = true;
if (_cb_start_global) {
_cb_start_global();
}
_raw_source = raw_source;
_userdata = userdata;
_cb_finished = callback_on_end;
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
_seq_position = 0;
_tone_ticks = 0;
// initiate 64 hz callback
_em_interval_id = emscripten_set_interval(cb_watch_buzzer_raw_source, (double)(1000/64), (void *)NULL);
}
void cb_watch_buzzer_raw_source(void *userData) {
// callback for reading the note sequence
(void) userData;
uint16_t period;
uint16_t duration;
bool done;
if (_tone_ticks == 0) {
done = _raw_source(_seq_position, _userdata, &period, &duration);
if (done || duration == 0) {
// end the sequence
watch_buzzer_abort_sequence();
} else {
if (period == WATCH_BUZZER_PERIOD_REST) {
watch_set_buzzer_off();
} else {
watch_set_buzzer_period_and_duty_cycle(period, _volume);
watch_set_buzzer_on();
}
// set duration ticks and move to next tone
_tone_ticks = duration - 1;
_seq_position += 1;
}
} else {
_tone_ticks--;
}
}
void watch_buzzer_abort_sequence(void) { void watch_buzzer_abort_sequence(void) {
// ends/aborts the sequence // ends/aborts the sequence
if (_em_interval_id) _em_interval_stop(); if (_em_interval_id) _em_interval_stop();
watch_set_buzzer_off(); watch_set_buzzer_off();
watch_disable_buzzer();
if (!_buzzer_is_active) {
return;
}
_buzzer_is_active = false;
if (_cb_stop_global) {
_cb_stop_global();
}
if (_cb_finished) {
_cb_finished();
}
}
void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop) {
_cb_start_global = cb_start;
_cb_stop_global = cb_stop;
} }
void watch_enable_buzzer(void) { void watch_enable_buzzer(void) {
watch_buzzer_abort_sequence();
buzzer_enabled = true; buzzer_enabled = true;
buzzer_period = NotePeriods[BUZZER_NOTE_A4]; buzzer_period = NotePeriods[BUZZER_NOTE_A4];
EM_ASM({ EM_ASM({
// "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)(); Module['audioContext'] = new (window.AudioContext || window.webkitAudioContext)();
}
}); });
} }
@@ -227,6 +126,13 @@ void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty_cycle)
void watch_disable_buzzer(void) { void watch_disable_buzzer(void) {
buzzer_enabled = false; buzzer_enabled = false;
buzzer_period = NotePeriods[BUZZER_NOTE_A4]; buzzer_period = NotePeriods[BUZZER_NOTE_A4];
EM_ASM({
if (Module['audioContext']) {
Module['audioContext'].close();
Module['audioContext'] = null;
}
});
} }
void watch_set_buzzer_on(void) { void watch_set_buzzer_on(void) {
@@ -269,17 +175,15 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms) {
} }
void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) { void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) {
static int8_t single_note_sequence[3]; 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();
}
single_note_sequence[0] = note; main_loop_sleep(duration_ms);
// 64 ticks per second for the tc0 watch_set_buzzer_off();
// Each tick is approximately 15ms
uint16_t duration = duration_ms / 15;
if (duration > 127) duration = 127;
single_note_sequence[1] = (int8_t)duration;
single_note_sequence[2] = 0;
watch_buzzer_play_sequence_with_volume(single_note_sequence, NULL, volume);
} }
void watch_enable_leds(void) {} void watch_enable_leds(void) {}