Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78bdbb0e15 |
@@ -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,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
File diff suppressed because it is too large
Load Diff
+23
-43
@@ -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
@@ -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_
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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, \
|
|
||||||
})
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
totp_face.secrets.h
|
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, \
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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_
|
|
||||||
|
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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 it’s
|
|
||||||
// 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 don’t 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 let’s leave a clean state.
|
|
||||||
_move_next_high_tide(state, _get_current_unix_time());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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, \
|
|
||||||
})
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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_
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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, \
|
|
||||||
})
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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__
|
||||||
|
|||||||
@@ -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/
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|
||||||
/** @} */
|
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|||||||
Reference in New Issue
Block a user