Compare commits
20 Commits
movement-2
...
52e3247f45
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52e3247f45 | ||
|
|
0e42b9dde0 | ||
|
|
e3d50374bf | ||
|
|
b9de5cf2e4 | ||
|
|
357335fa2d | ||
|
|
1d763d17fe | ||
|
|
d8c9a5f138 | ||
|
|
4335ec6c95 | ||
|
|
3d41a59eaa | ||
|
|
8b19ce4f1e | ||
|
|
e8f31beb70 | ||
|
|
c2103d9eaa | ||
|
|
88338dc0ba | ||
|
|
e8ba597131 | ||
|
|
52c3d5b796 | ||
|
|
7af5626147 | ||
|
|
b2d313e0e7 | ||
|
|
0f5defe789 | ||
|
|
3634460a02 | ||
|
|
fc2f9c5130 |
3704
movement/lib/smallchesslib/smallchesslib.h
Normal file
3704
movement/lib/smallchesslib/smallchesslib.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ INCLUDES += \
|
||||
-I../lib/vsop87/ \
|
||||
-I../lib/astrolib/ \
|
||||
-I../lib/morsecalc/ \
|
||||
-I../lib/smallchesslib/ \
|
||||
|
||||
# If you add any other source files you wish to compile, add them after ../app.c
|
||||
# Note that you will need to add a backslash at the end of any line you wish to continue, i.e.
|
||||
@@ -119,6 +120,7 @@ SRCS += \
|
||||
../watch_faces/complication/toss_up_face.c \
|
||||
../watch_faces/complication/geomancy_face.c \
|
||||
../watch_faces/clock/simple_clock_bin_led_face.c \
|
||||
../watch_faces/complication/menstrual_cycle_face.c \
|
||||
../watch_faces/complication/flashlight_face.c \
|
||||
../watch_faces/clock/decimal_time_face.c \
|
||||
../watch_faces/clock/wyoscan_face.c \
|
||||
@@ -129,7 +131,10 @@ SRCS += \
|
||||
../watch_faces/complication/couch_to_5k_face.c \
|
||||
../watch_faces/clock/minute_repeater_decimal_face.c \
|
||||
../watch_faces/complication/tuning_tones_face.c \
|
||||
../watch_faces/sensor/minmax_face.c \
|
||||
../watch_faces/complication/kitchen_conversions_face.c \
|
||||
../watch_faces/complication/butterfly_game_face.c \
|
||||
../watch_faces/complication/wareki_face.c \
|
||||
../watch_faces/complication/wordle_face.c \
|
||||
../watch_faces/complication/endless_runner_face.c \
|
||||
../watch_faces/complication/periodic_face.c \
|
||||
@@ -141,6 +146,10 @@ SRCS += \
|
||||
../watch_faces/complication/simple_calculator_face.c \
|
||||
../watch_faces/sensor/alarm_thermometer_face.c \
|
||||
../watch_faces/demo/beeps_face.c \
|
||||
../watch_faces/sensor/accel_interrupt_count_face.c \
|
||||
../watch_faces/complication/metronome_face.c \
|
||||
../watch_faces/complication/smallchess_face.c \
|
||||
../watch_faces/complication/sunrise_sunset_alt_face.c \
|
||||
# New watch faces go above this line.
|
||||
|
||||
# Leave this line at the bottom of the file; it has all the targets for making your project.
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
#include "geomancy_face.h"
|
||||
#include "dual_timer_face.h"
|
||||
#include "simple_clock_bin_led_face.h"
|
||||
#include "menstrual_cycle_face.h"
|
||||
#include "flashlight_face.h"
|
||||
#include "decimal_time_face.h"
|
||||
#include "wyoscan_face.h"
|
||||
@@ -104,7 +105,10 @@
|
||||
#include "couch_to_5k_face.h"
|
||||
#include "minute_repeater_decimal_face.h"
|
||||
#include "tuning_tones_face.h"
|
||||
#include "minmax_face.h"
|
||||
#include "kitchen_conversions_face.h"
|
||||
#include "butterfly_game_face.h"
|
||||
#include "wareki_face.h"
|
||||
#include "wordle_face.h"
|
||||
#include "endless_runner_face.h"
|
||||
#include "periodic_face.h"
|
||||
@@ -116,6 +120,10 @@
|
||||
#include "simple_calculator_face.h"
|
||||
#include "alarm_thermometer_face.h"
|
||||
#include "beeps_face.h"
|
||||
#include "accel_interrupt_count_face.h"
|
||||
#include "metronome_face.h"
|
||||
#include "smallchess_face.h"
|
||||
#include "sunrise_sunset_alt_face.h"
|
||||
// New includes go above this line.
|
||||
|
||||
#endif // MOVEMENT_FACES_H_
|
||||
|
||||
467
movement/watch_faces/complication/butterfly_game_face.c
Normal file
467
movement/watch_faces/complication/butterfly_game_face.c
Normal file
@@ -0,0 +1,467 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Hugo Chargois
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
// Emulator only: need time() to seed the random number generator
|
||||
#if __EMSCRIPTEN__
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "butterfly_game_face.h"
|
||||
|
||||
static char butterfly_shapes[][3] = {
|
||||
"[]", "][", "25", "52", "9e", "e9", "6a", "a6", "3E", "E3", "00", "HH", "88"
|
||||
};
|
||||
|
||||
static int8_t single_beep[] = {BUZZER_NOTE_A7, 4, 0};
|
||||
static int8_t round_win_melody[] = {
|
||||
BUZZER_NOTE_C6, 4,
|
||||
BUZZER_NOTE_E6, 4,
|
||||
BUZZER_NOTE_G6, 4,
|
||||
BUZZER_NOTE_C7, 12,
|
||||
0};
|
||||
static int8_t round_lose_melody[] = {
|
||||
BUZZER_NOTE_E6, 4,
|
||||
BUZZER_NOTE_F6, 4,
|
||||
BUZZER_NOTE_D6SHARP_E6FLAT, 4,
|
||||
BUZZER_NOTE_C6, 12,
|
||||
0};
|
||||
static int8_t game_win_melody[] = {
|
||||
BUZZER_NOTE_G6, 4,
|
||||
BUZZER_NOTE_A6, 4,
|
||||
BUZZER_NOTE_B6, 4,
|
||||
BUZZER_NOTE_C7, 12,
|
||||
BUZZER_NOTE_D7, 4,
|
||||
BUZZER_NOTE_E7, 4,
|
||||
BUZZER_NOTE_D7, 4,
|
||||
BUZZER_NOTE_C7, 12,
|
||||
BUZZER_NOTE_B6, 4,
|
||||
BUZZER_NOTE_C7, 4,
|
||||
BUZZER_NOTE_D7, 4,
|
||||
BUZZER_NOTE_G7, 24,
|
||||
0};
|
||||
|
||||
#define NUM_SHAPES (sizeof(butterfly_shapes) / sizeof(butterfly_shapes[0]))
|
||||
|
||||
#define POS_LEFT 4
|
||||
#define POS_CENTER 6
|
||||
#define POS_RIGHT 8
|
||||
|
||||
#define TICK_FREQ 8
|
||||
#define TICKS_PER_SHAPE 8
|
||||
|
||||
#define PLAYER_1 0
|
||||
#define PLAYER_2 1
|
||||
|
||||
|
||||
// returns a random integer r with 0 <= r < max
|
||||
static inline uint8_t _get_rand(uint8_t max) {
|
||||
#if __EMSCRIPTEN__
|
||||
return rand() % max;
|
||||
#else
|
||||
return arc4random_uniform(max);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* The game is built with a simple state machine where each state is called a
|
||||
* "screen". Each screen can draw on the display and handles events, including
|
||||
* the "activate" event, which is repurposed and sent whenever we move from one
|
||||
* screen to another via the _transition_to function. Basically it's a mini
|
||||
* movement inside movement.
|
||||
*/
|
||||
typedef bool (*screen_fn_t)(movement_event_t, butterfly_game_state_t*);
|
||||
|
||||
static screen_fn_t cur_screen_fn;
|
||||
|
||||
static bool _transition_to(screen_fn_t sf, butterfly_game_state_t *state) {
|
||||
movement_event_t ev = {EVENT_ACTIVATE, 0};
|
||||
cur_screen_fn = sf;
|
||||
return sf(ev, state);
|
||||
}
|
||||
|
||||
static uint8_t _pick_wrong_shape(butterfly_game_state_t *state, bool skip_wrong_shape) {
|
||||
if (!skip_wrong_shape) {
|
||||
// easy case, we only need to skip over 1 shape: the correct shape
|
||||
uint8_t r = _get_rand(NUM_SHAPES-1);
|
||||
if (r >= state->correct_shape) {
|
||||
r++;
|
||||
}
|
||||
return r;
|
||||
} else {
|
||||
// a bit more complex, we need to skip over 2 shapes: the correct one
|
||||
// and the current wrong one
|
||||
uint8_t r = _get_rand(NUM_SHAPES-2);
|
||||
uint8_t i1, i2; // the 2 indices to skip over, with i1 < i2
|
||||
if (state->correct_shape < state->current_shape) {
|
||||
i1 = state->correct_shape;
|
||||
i2 = state->current_shape;
|
||||
} else {
|
||||
i1 = state->current_shape;
|
||||
i2 = state->correct_shape;
|
||||
}
|
||||
|
||||
if (r >= i1) {
|
||||
r++;
|
||||
}
|
||||
if (r >= i2) {
|
||||
r++;
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
static void _display_shape(uint8_t shape, uint8_t pos) {
|
||||
watch_display_string(butterfly_shapes[shape], pos);
|
||||
}
|
||||
|
||||
static void _display_scores(butterfly_game_state_t *state) {
|
||||
char buf[] = " ";
|
||||
buf[0] = '0' + state->score_p1;
|
||||
watch_display_string(buf, 0);
|
||||
buf[0] = '0' + state->score_p2;
|
||||
watch_display_string(buf, 3);
|
||||
}
|
||||
|
||||
static void _play_sound(butterfly_game_state_t *state, int8_t *seq) {
|
||||
if (state->sound) watch_buzzer_play_sequence(seq, NULL);
|
||||
}
|
||||
|
||||
static bool _round_start_screen(movement_event_t event, butterfly_game_state_t *state);
|
||||
static bool _reset_screen(movement_event_t event, butterfly_game_state_t *state);
|
||||
|
||||
static bool _game_win_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->ctr = 4 * TICK_FREQ;
|
||||
watch_clear_display();
|
||||
|
||||
if (state->score_p1 >= state->goal_score) {
|
||||
watch_display_string("pl1 wins", 0);
|
||||
} else {
|
||||
watch_display_string("pl2 wins", 0);
|
||||
}
|
||||
_play_sound(state, game_win_melody);
|
||||
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
state->ctr--;
|
||||
if (state->ctr == 0) {
|
||||
return _transition_to(_reset_screen, state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _round_win_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->ctr = TICK_FREQ;
|
||||
|
||||
if (state->round_winner == PLAYER_1) {
|
||||
state->score_p1++;
|
||||
} else {
|
||||
state->score_p2++;
|
||||
}
|
||||
|
||||
watch_clear_display();
|
||||
_display_scores(state);
|
||||
_display_shape(state->correct_shape, state->round_winner == PLAYER_1 ? POS_LEFT : POS_RIGHT);
|
||||
_play_sound(state, round_win_melody);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
state->ctr--;
|
||||
if (state->ctr == 0) {
|
||||
if (state->score_p1 >= state->goal_score || state->score_p2 >= state->goal_score) {
|
||||
return _transition_to(_game_win_screen, state);
|
||||
}
|
||||
return _transition_to(_round_start_screen, state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _round_lose_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->ctr = TICK_FREQ;
|
||||
|
||||
if (state->round_winner == PLAYER_1) {
|
||||
if (state->score_p2 > 0) state->score_p2--;
|
||||
} else {
|
||||
if (state->score_p1 > 0) state->score_p1--;
|
||||
}
|
||||
|
||||
_display_shape(state->correct_shape, POS_CENTER);
|
||||
_play_sound(state, round_lose_melody);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (--state->ctr == 0) {
|
||||
return _transition_to(_round_start_screen, state);
|
||||
}
|
||||
_display_shape(state->ctr%2 ? state->correct_shape : state->current_shape, POS_CENTER);
|
||||
break;
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _correct_shape_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
_display_shape(state->correct_shape, POS_CENTER);
|
||||
_play_sound(state, single_beep);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
state->round_winner = PLAYER_1;
|
||||
return _transition_to(_round_win_screen, state);
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->round_winner = PLAYER_2;
|
||||
return _transition_to(_round_win_screen, state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _wrong_shape_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->ctr = TICKS_PER_SHAPE;
|
||||
state->current_shape = _pick_wrong_shape(state, true);
|
||||
_display_shape(state->current_shape, POS_CENTER);
|
||||
_play_sound(state, single_beep);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (--state->ctr == 0) {
|
||||
if (--state->show_correct_shape_after == 0) {
|
||||
return _transition_to(_correct_shape_screen, state);
|
||||
}
|
||||
return _transition_to(_wrong_shape_screen, state);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
state->round_winner = PLAYER_2;
|
||||
return _transition_to(_round_lose_screen, state);
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->round_winner = PLAYER_1;
|
||||
return _transition_to(_round_lose_screen, state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _first_wrong_shape_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
// the first of the wrong shape screens is a bit different than the next
|
||||
// ones, for 2 reasons:
|
||||
// * we can pick any shape except one (the correct shape); whereas in the
|
||||
// subsequent wrong shape screens, we also must not pick the same wrong
|
||||
// shape as the last
|
||||
// * we don't act on the light/alarm button events; they would normally be
|
||||
// a fail in a wrong shape screen, but in this case it may just be that
|
||||
// the 2 players acknowledge the picked shape (in the previous screen) in
|
||||
// quick succession, and we don't want the second player to immediately
|
||||
// fail.
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->ctr = TICKS_PER_SHAPE;
|
||||
state->current_shape = _pick_wrong_shape(state, false);
|
||||
_display_shape(state->current_shape, POS_CENTER);
|
||||
_play_sound(state, single_beep);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (--state->ctr == 0) {
|
||||
return _transition_to(_wrong_shape_screen, state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _round_start_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->correct_shape = _get_rand(NUM_SHAPES);
|
||||
state->show_correct_shape_after = _get_rand(10) + 1;
|
||||
watch_display_string(" - -", 0);
|
||||
_display_scores(state);
|
||||
_display_shape(state->correct_shape, POS_CENTER);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
watch_display_string(" ", 4);
|
||||
return _transition_to(_first_wrong_shape_screen, state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _goal_select_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
watch_clear_display();
|
||||
state->goal_score = 6;
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
return _transition_to(_round_start_screen, state);
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->goal_score += 3;
|
||||
if (state->goal_score > 9) state->goal_score = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
char buf[] = "GOaL ";
|
||||
buf[5] = '0' + state->goal_score;
|
||||
watch_display_string(buf, 4);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _reset_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
(void) event;
|
||||
|
||||
state->score_p1 = 0;
|
||||
state->score_p2 = 0;
|
||||
|
||||
return _transition_to(_goal_select_screen, state);
|
||||
}
|
||||
|
||||
static bool _continue_select_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
watch_clear_display();
|
||||
|
||||
// no game in progress, start a new game
|
||||
if (state->score_p1 == 0 && state->score_p2 == 0) {
|
||||
return _transition_to(_goal_select_screen, state);
|
||||
}
|
||||
|
||||
state->cont = false;
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
if (state->cont) {
|
||||
return _transition_to(_round_start_screen, state);
|
||||
}
|
||||
return _transition_to(_reset_screen, state);
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->cont = !state->cont;
|
||||
break;
|
||||
}
|
||||
|
||||
if (state->cont) {
|
||||
watch_display_string("Cont y", 4);
|
||||
} else {
|
||||
watch_display_string("Cont n", 4);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _sound_select_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
watch_clear_display();
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
return _transition_to(_continue_select_screen, state);
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
state->sound = !state->sound;
|
||||
break;
|
||||
}
|
||||
|
||||
if (state->sound) {
|
||||
watch_display_string("snd y", 5);
|
||||
} else {
|
||||
watch_display_string("snd n", 5);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _splash_screen(movement_event_t event, butterfly_game_state_t *state) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
state->ctr = TICK_FREQ;
|
||||
|
||||
watch_clear_display();
|
||||
watch_display_string("Btrfly", 4);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
return _transition_to(_sound_select_screen, state);
|
||||
case EVENT_TICK:
|
||||
if (--state->ctr == 0) {
|
||||
return _transition_to(_sound_select_screen, state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void butterfly_game_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
|
||||
(void) settings;
|
||||
(void) watch_face_index;
|
||||
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(butterfly_game_state_t));
|
||||
memset(*context_ptr, 0, sizeof(butterfly_game_state_t));
|
||||
// Do any one-time tasks in here; the inside of this conditional happens only at boot.
|
||||
}
|
||||
// Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.
|
||||
#if __EMSCRIPTEN__
|
||||
// simulator only: seed the random number generator
|
||||
time_t t;
|
||||
srand((unsigned) time(&t));
|
||||
#endif
|
||||
}
|
||||
|
||||
void butterfly_game_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
|
||||
movement_request_tick_frequency(TICK_FREQ);
|
||||
}
|
||||
|
||||
bool butterfly_game_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
butterfly_game_state_t *state = (butterfly_game_state_t *)context;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
return _transition_to(_splash_screen, state);
|
||||
case EVENT_TICK:
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
return (*cur_screen_fn)(event, state);
|
||||
case EVENT_TIMEOUT:
|
||||
movement_move_to_face(0);
|
||||
return true;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
}
|
||||
|
||||
void butterfly_game_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
|
||||
// handle any cleanup before your watch face goes off-screen.
|
||||
}
|
||||
|
||||
125
movement/watch_faces/complication/butterfly_game_face.h
Normal file
125
movement/watch_faces/complication/butterfly_game_face.h
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Hugo Chargois
|
||||
*
|
||||
* 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 BUTTERFLY_GAME_FACE_H_
|
||||
#define BUTTERFLY_GAME_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* BUTTERFLY
|
||||
*
|
||||
* A GAME OF SHAPE RECOGNITION AND QUICK REFLEXES FOR 2 PLAYERS
|
||||
*
|
||||
* Setup
|
||||
* =====
|
||||
*
|
||||
* The game is played by 2 players, each using a distinct button:
|
||||
* - player 1 plays with the LIGHT (upper left) button
|
||||
* - player 2 plays with the ALARM (lower right) button
|
||||
*
|
||||
* To play, both players need a firm grip on the watch. A suggested method is to
|
||||
* face each other, remove the watch from the wrist, and position it sideways
|
||||
* between you. Hold one side of the strap in your preferred hand (right or
|
||||
* left) and use your thumb to play.
|
||||
*
|
||||
* Start of the game
|
||||
* =================
|
||||
*
|
||||
* After the splash screen (BtrFly) is shown, the game proceeds through a couple
|
||||
* configuration screens. Use ALARM to cycle through the possible values, and
|
||||
* LIGHT to validate and move to the next screen.
|
||||
*
|
||||
* The configuration options are:
|
||||
*
|
||||
* - snd y/n Toggle sound effects on or off
|
||||
* - goal 3/6/9 Choose to play a game of 3, 6 or 9 points
|
||||
* - cont y/n Decide to continue an unfinished game or start a new one
|
||||
* (this option appears only if a game is in progress)
|
||||
*
|
||||
* Rules
|
||||
* =====
|
||||
*
|
||||
* Prior to each round, a symmetrical shape composed of 2 characters is shown in
|
||||
* the center of the screen. This shape, representing a butterfly's wings, is
|
||||
* randomly chosen from a set of a dozen or so possible shapes. For example:
|
||||
*
|
||||
* ][
|
||||
*
|
||||
* Memorize this shape! Your objective in the round will be to "catch" this
|
||||
* "butterfly" by pressing your button before your opponent does.
|
||||
*
|
||||
* Once you believe you've memorized the shape, press your button. The round
|
||||
* officially begins as soon as either player presses their button.
|
||||
*
|
||||
* Various "butterflies" will then appear on the screen, one after the other.
|
||||
* The fastest player to press their button when the correct butterfly is shown
|
||||
* wins the round. However, if a player presses their button when an incorrect
|
||||
* butterfly is shown, they immediately lose the round.
|
||||
*
|
||||
* Scoring
|
||||
* =======
|
||||
*
|
||||
* The scores are displayed at the top of the screen at all times.
|
||||
*
|
||||
* When a round is won by a player, their score increases by one. When a round
|
||||
* is lost by a player, their score decreases by one; unless they have a score
|
||||
* of 0, in which case it remains unchanged.
|
||||
*
|
||||
* The game ends when a player reaches the set point goal (3, 6 or 9 points).
|
||||
*
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
bool cont : 1; // continue
|
||||
bool sound : 1;
|
||||
uint8_t goal_score : 4;
|
||||
|
||||
// a generic ctr used by multiple states to display themselves for multiple frames
|
||||
uint8_t ctr : 6;
|
||||
|
||||
uint8_t correct_shape : 5;
|
||||
uint8_t current_shape : 5;
|
||||
uint8_t show_correct_shape_after : 5;
|
||||
uint8_t round_winner : 1;
|
||||
|
||||
uint8_t score_p1 : 5;
|
||||
uint8_t score_p2 : 5;
|
||||
} butterfly_game_state_t;
|
||||
|
||||
void butterfly_game_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void butterfly_game_face_activate(movement_settings_t *settings, void *context);
|
||||
bool butterfly_game_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void butterfly_game_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define butterfly_game_face ((const watch_face_t){ \
|
||||
butterfly_game_face_setup, \
|
||||
butterfly_game_face_activate, \
|
||||
butterfly_game_face_loop, \
|
||||
butterfly_game_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // BUTTERFLY_GAME_FACE_H_
|
||||
|
||||
472
movement/watch_faces/complication/menstrual_cycle_face.c
Normal file
472
movement/watch_faces/complication/menstrual_cycle_face.c
Normal file
@@ -0,0 +1,472 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Joseph Borne Komosa | @jokomo24
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*
|
||||
* Menstrual Cycle Face
|
||||
*
|
||||
* Background:
|
||||
*
|
||||
* I discovered the Casio F-91W through my partner, appreciated the retro aesthetic of the watch,
|
||||
* and got one for myself. Soon afterward I discovered the Sensor Watch project and ordered two boards!
|
||||
* I introduced the Sensor Watch to my partner who inquired whether she could track her menstrual cycle.
|
||||
* So I decided to implement a menstrual cycle watch face that also calculates the peak fertility window
|
||||
* using The Calendar Method. While this information may be useful when attempting to achieve or avoid
|
||||
* pregnancy, it is important to understand that these are rough estimates at best.
|
||||
*
|
||||
* How to use:
|
||||
*
|
||||
* 1. To begin tracking, go to 'Last Period' page and toggle the alarm button to the number of days since
|
||||
* the last, most recent, period and hold the alarm button to enter. This will perform the following actions:
|
||||
* - Store the corresponding date as the 'first' period in order to calculate the total_days_tracked.
|
||||
* - Turn on the Signal Indicator to signify that tracking has been activated.
|
||||
* - Deactivate this page and instead show the ticking animation.
|
||||
* - Adjust the days left in the 'Period in <num> Days' page accordingly.
|
||||
* - Activate the 'Period Is Here' page and no longer display 'NA'. To prevent accidental user entry,
|
||||
* the page will display the ticking animation until ten days have passed since the date of the last
|
||||
* period entered.
|
||||
* - Activate the 'Peak Fertility' page to begin showing the estimated window,
|
||||
* as well as display the Alarm Indicator, on this page and on the main 'Period in <num> Days' page,
|
||||
* whenever the current date falls within the Peak Fertility Window.
|
||||
*
|
||||
* 2. Toggle and enter 'y' in the 'Period Is Here' page on the day of every sequential period afterward.
|
||||
* DO NOT FORGET TO DO SO!
|
||||
* - If forgotten, the data will become inaccurate and tracking will need to be reset! -> (FIXME, allow one to enter a 'missed' period using the 'Last Period' page).
|
||||
* This will perform the following actions:
|
||||
* - Calculate this completed cycle's length and reevaluate the shortest and longest cycle variables.
|
||||
* - Increment total_cycles by one.
|
||||
* - Recalculate and save the average cycle for 'Average Cycle' page.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "menstrual_cycle_face.h"
|
||||
#include "watch.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
#define TYPICAL_AVG_CYC 28
|
||||
#define SECONDS_PER_DAY 86400
|
||||
|
||||
#define MENSTRUAL_CYCLE_FACE_NUM_PAGES (6)
|
||||
enum {
|
||||
period_in_num_days,
|
||||
average_cycle,
|
||||
peak_fertility_window,
|
||||
period_is_here,
|
||||
first_period,
|
||||
reset,
|
||||
} page_titles_e;
|
||||
const char menstrual_cycle_face_titles[MENSTRUAL_CYCLE_FACE_NUM_PAGES][11] = {
|
||||
"Prin day", // Period In <num> Days: Estimated days till the next period occurs
|
||||
"Av cycle ", // Average Cycle: The average number of days estimated per cycle
|
||||
"Peak Fert ", // Peak Fertility Window: The first and last day of month (displayed top & bottom right, respectively, once tracking) for the estimated window of fertility
|
||||
"Prishere ", // Period Is Here: Toggle and enter 'y' on the day the actual period occurs to improve Avg and Fert estimations
|
||||
"Last Per ", // Last Period: Enter the number of days since the last period to begin tracking from that corresponding date by storing it as the 'first'
|
||||
" Reset ", // Reset: Toggle and enter 'y' to reset tracking data
|
||||
};
|
||||
|
||||
/* Beep function */
|
||||
static inline void beep(movement_settings_t *settings) {
|
||||
if (settings->bit.button_should_sound)
|
||||
watch_buzzer_play_note(BUZZER_NOTE_E8, 75);
|
||||
}
|
||||
|
||||
// Calculate the total number of days for which menstrual cycle tracking has been active
|
||||
static inline uint32_t total_days_tracked(menstrual_cycle_state_t *state) {
|
||||
|
||||
// If tracking has not yet been activated, return 0
|
||||
if (!(state->dates.reg))
|
||||
return 0;
|
||||
|
||||
// Otherwise, set the start date to the first day of the first tracked cycle
|
||||
watch_date_time date_time_start;
|
||||
date_time_start.unit.second = 0;
|
||||
date_time_start.unit.minute = 0;
|
||||
date_time_start.unit.hour = 0;
|
||||
date_time_start.unit.day = state->dates.bit.first_day;
|
||||
date_time_start.unit.month = state->dates.bit.first_month;
|
||||
date_time_start.unit.year = state->dates.bit.first_year;
|
||||
|
||||
// Get the current date and time
|
||||
watch_date_time date_time_now = watch_rtc_get_date_time();
|
||||
|
||||
// Convert the start date and current date to Unix time
|
||||
uint32_t unix_start = watch_utility_date_time_to_unix_time(date_time_start, state->utc_offset);
|
||||
uint32_t unix_now = watch_utility_date_time_to_unix_time(date_time_now, state->utc_offset);
|
||||
|
||||
// Calculate the total number of days and return it
|
||||
return (unix_now - unix_start) / SECONDS_PER_DAY;
|
||||
}
|
||||
|
||||
// Calculate the number of days until the next menstrual period
|
||||
static inline int8_t days_till_period(menstrual_cycle_state_t *state) {
|
||||
|
||||
// Calculate the number of days left until the next period based on the average cycle length and the number of cycles tracked
|
||||
int8_t days_left = (state->cycles.bit.average_cycle * (state->cycles.bit.total_cycles + 1)) - total_days_tracked(state);
|
||||
|
||||
// If the result is negative, return 0 (i.e., the period is expected to start today or has already started)
|
||||
return (days_left < 0) ? 0 : days_left;
|
||||
}
|
||||
|
||||
static inline void reset_tracking(menstrual_cycle_state_t *state) {
|
||||
|
||||
state->dates.bit.first_day = 0;
|
||||
state->dates.bit.first_month = 0;
|
||||
state->dates.bit.first_year = 0;
|
||||
|
||||
state->dates.bit.prev_day = 0;
|
||||
state->dates.bit.prev_month = 0;
|
||||
state->dates.bit.prev_year = 0;
|
||||
|
||||
state->cycles.bit.shortest_cycle = TYPICAL_AVG_CYC;
|
||||
state->cycles.bit.longest_cycle = TYPICAL_AVG_CYC;
|
||||
state->cycles.bit.average_cycle = TYPICAL_AVG_CYC;
|
||||
state->cycles.bit.total_cycles = 0;
|
||||
|
||||
state->dates.bit.reserved = 0;
|
||||
state->cycles.bit.reserved = 0;
|
||||
|
||||
watch_store_backup_data(state->dates.reg, state->backup_register_dt);
|
||||
watch_store_backup_data(state->cycles.reg, state->backup_register_cy);
|
||||
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
}
|
||||
|
||||
/*
|
||||
Fertility Window based on "The Calendar Method"
|
||||
Source: https://www.womenshealth.gov/pregnancy/you-get-pregnant/trying-conceive
|
||||
|
||||
The Calendar Method has several steps:
|
||||
|
||||
Step 1: Track the menstrual cycle for 8–12 months. One cycle is from the first day of one
|
||||
period until the first day of the next period. The average cycle is 28 days, but
|
||||
it may be as short as 24 days or as long as 38 days.
|
||||
Step 2: Subtract 18 from the number of days in the shortest menstrual cycle.
|
||||
Step 3: Subtract 11 from the number of days in the longest menstrual cycle.
|
||||
Step 4: Using a calendar, mark down the start of the next period (using previous instead). Count ahead by the number
|
||||
of days calculated in step 2. This is when peak fertility begins. Peak fertility ends
|
||||
at the number of days calculated in step 3.
|
||||
NOTE: Right now, the fertility window face displays its estimated window as soon as tracking is activated, although
|
||||
it is important to keep in mind that The Calendar Method states that peak accuracy of the window will be
|
||||
reached only after at least 8 months of tracking the menstrual cycle (can make it so that it only displays
|
||||
after total_days_tracked >= 8 months...but the info is interesting and should already be taken with the understanding that,
|
||||
in general, it is a rough estimation at best).
|
||||
*/
|
||||
typedef enum Fertile_Window {first_day, last_day} fertile_window;
|
||||
// Calculate the predicted starting or ending day of peak fertility
|
||||
static inline uint32_t get_day_pk_fert(menstrual_cycle_state_t *state, fertile_window which_day) {
|
||||
|
||||
// Get the date of the previous period
|
||||
watch_date_time date_prev_period;
|
||||
date_prev_period.unit.second = 0;
|
||||
date_prev_period.unit.minute = 0;
|
||||
date_prev_period.unit.hour = 0;
|
||||
date_prev_period.unit.day = state->dates.bit.prev_day;
|
||||
date_prev_period.unit.month = state->dates.bit.prev_month;
|
||||
date_prev_period.unit.year = state->dates.bit.prev_year;
|
||||
|
||||
// Convert the previous period date to Unix time
|
||||
uint32_t unix_prev_period = watch_utility_date_time_to_unix_time(date_prev_period, state->utc_offset);
|
||||
|
||||
// Calculate the Unix time of the predicted peak fertility day based on the length of the shortest/longest cycle
|
||||
uint32_t unix_pk_date;
|
||||
switch(which_day) {
|
||||
case first_day:
|
||||
unix_pk_date = unix_prev_period + ((state->cycles.bit.shortest_cycle - 18) * SECONDS_PER_DAY);
|
||||
break;
|
||||
case last_day:
|
||||
unix_pk_date = unix_prev_period + ((state->cycles.bit.longest_cycle - 11) * SECONDS_PER_DAY);
|
||||
break;
|
||||
}
|
||||
|
||||
// Convert the Unix time of the predicted peak fertility day to a date/time and return the day of the month
|
||||
return watch_utility_date_time_from_unix_time(unix_pk_date, state->utc_offset).unit.day;
|
||||
}
|
||||
|
||||
// Determine if today falls within the predicted peak fertility window
|
||||
static inline bool inside_fert_window(menstrual_cycle_state_t *state) {
|
||||
|
||||
// If tracking has not yet been activated, return false
|
||||
if (!(state->dates.reg))
|
||||
return false;
|
||||
|
||||
// Get the current date/time
|
||||
watch_date_time date_time_now = watch_rtc_get_date_time();
|
||||
|
||||
// Check if the current day falls between the first and last predicted peak fertility days
|
||||
if (get_day_pk_fert(state, first_day) > get_day_pk_fert(state, last_day)) { // We are crossing over the end of the month
|
||||
if (date_time_now.unit.day >= get_day_pk_fert(state, first_day) ||
|
||||
date_time_now.unit.day <= get_day_pk_fert(state, last_day))
|
||||
return true;
|
||||
}
|
||||
else if (date_time_now.unit.day >= get_day_pk_fert(state, first_day) &&
|
||||
date_time_now.unit.day <= get_day_pk_fert(state, last_day))
|
||||
return true;
|
||||
// If the current day does not fall within the predicted peak fertility window, return false
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the shortest and longest menstrual cycles based on the previous menstrual cycle
|
||||
static inline void update_shortest_longest_cycle(menstrual_cycle_state_t *state) {
|
||||
|
||||
// Get the date of the previous menstrual cycle
|
||||
watch_date_time date_prev_period;
|
||||
date_prev_period.unit.second = 0;
|
||||
date_prev_period.unit.minute = 0;
|
||||
date_prev_period.unit.hour = 0;
|
||||
date_prev_period.unit.day = state->dates.bit.prev_day;
|
||||
date_prev_period.unit.month = state->dates.bit.prev_month;
|
||||
date_prev_period.unit.year = state->dates.bit.prev_year;
|
||||
|
||||
// Convert the date of the previous menstrual cycle to UNIX time
|
||||
uint32_t unix_prev_period = watch_utility_date_time_to_unix_time(date_prev_period, state->utc_offset);
|
||||
|
||||
// Calculate the length of the current menstrual cycle
|
||||
uint8_t cycle_length = total_days_tracked(state) - (unix_prev_period / SECONDS_PER_DAY);
|
||||
|
||||
// Update the shortest or longest cycle length if necessary
|
||||
if (cycle_length < state->cycles.bit.shortest_cycle)
|
||||
state->cycles.bit.shortest_cycle = cycle_length;
|
||||
else if (cycle_length > state->cycles.bit.longest_cycle)
|
||||
state->cycles.bit.longest_cycle = cycle_length;
|
||||
}
|
||||
|
||||
void menstrual_cycle_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
|
||||
(void) watch_face_index;
|
||||
(void) settings;
|
||||
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(menstrual_cycle_state_t));
|
||||
memset(*context_ptr, 0, sizeof(menstrual_cycle_state_t));
|
||||
menstrual_cycle_state_t *state = ((menstrual_cycle_state_t *)*context_ptr);
|
||||
|
||||
state->dates.bit.first_day = 0;
|
||||
state->dates.bit.first_month = 0;
|
||||
state->dates.bit.first_year = 0;
|
||||
|
||||
state->dates.bit.prev_day = 0;
|
||||
state->dates.bit.prev_month = 0;
|
||||
state->dates.bit.prev_year = 0;
|
||||
|
||||
state->cycles.bit.shortest_cycle = TYPICAL_AVG_CYC;
|
||||
state->cycles.bit.longest_cycle = TYPICAL_AVG_CYC;
|
||||
state->cycles.bit.average_cycle = TYPICAL_AVG_CYC;
|
||||
state->cycles.bit.total_cycles = 0;
|
||||
|
||||
state->dates.bit.reserved = 0;
|
||||
state->cycles.bit.reserved = 0;
|
||||
|
||||
state->backup_register_dt = 0;
|
||||
state->backup_register_cy = 0;
|
||||
}
|
||||
|
||||
menstrual_cycle_state_t *state = ((menstrual_cycle_state_t *)*context_ptr);
|
||||
if (!(state->backup_register_dt && state->backup_register_cy)) {
|
||||
state->backup_register_dt = movement_claim_backup_register();
|
||||
state->backup_register_cy = movement_claim_backup_register();
|
||||
|
||||
if (state->backup_register_dt && state->backup_register_cy) {
|
||||
watch_store_backup_data(state->dates.reg, state->backup_register_dt);
|
||||
watch_store_backup_data(state->cycles.reg, state->backup_register_cy);
|
||||
}
|
||||
}
|
||||
else {
|
||||
state->dates.reg = watch_get_backup_data(state->backup_register_dt);
|
||||
state->cycles.reg = watch_get_backup_data(state->backup_register_cy);
|
||||
}
|
||||
}
|
||||
|
||||
void menstrual_cycle_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
menstrual_cycle_state_t *state = (menstrual_cycle_state_t *)context;
|
||||
state->period_today = 0;
|
||||
state->current_page = 0;
|
||||
state->reset_tracking = 0;
|
||||
state->utc_offset = movement_timezone_offsets[settings->bit.time_zone] * 60;
|
||||
movement_request_tick_frequency(4); // we need to manually blink some pixels
|
||||
}
|
||||
|
||||
bool menstrual_cycle_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
menstrual_cycle_state_t *state = (menstrual_cycle_state_t *)context;
|
||||
watch_date_time date_period;
|
||||
uint8_t current_page = state->current_page;
|
||||
uint8_t first_day_fert;
|
||||
uint8_t last_day_fert;
|
||||
uint32_t unix_now;
|
||||
uint32_t unix_prev_period;
|
||||
switch (event.event_type) {
|
||||
case EVENT_TICK:
|
||||
case EVENT_ACTIVATE:
|
||||
// Do nothing; handled below.
|
||||
break;
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
movement_move_to_next_face();
|
||||
return false;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
current_page = (current_page + 1) % MENSTRUAL_CYCLE_FACE_NUM_PAGES;
|
||||
state->current_page = current_page;
|
||||
state->days_prev_period = 0;
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
if (watch_tick_animation_is_running())
|
||||
watch_stop_tick_animation();
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
switch (current_page) {
|
||||
case period_in_num_days:
|
||||
break;
|
||||
case average_cycle:
|
||||
break;
|
||||
case peak_fertility_window:
|
||||
break;
|
||||
case period_is_here:
|
||||
if (state->period_today && total_days_tracked(state)) {
|
||||
// Calculate before updating date of last period
|
||||
update_shortest_longest_cycle(state);
|
||||
// Update the date of last period after calulating the, now previous, cycle length
|
||||
date_period = watch_rtc_get_date_time();
|
||||
state->dates.bit.prev_day = date_period.unit.day;
|
||||
state->dates.bit.prev_month = date_period.unit.month;
|
||||
state->dates.bit.prev_year = date_period.unit.year;
|
||||
// Calculate new cycle average
|
||||
state->cycles.bit.total_cycles += 1;
|
||||
state->cycles.bit.average_cycle = total_days_tracked(state) / state->cycles.bit.total_cycles;
|
||||
// Store the new data
|
||||
watch_store_backup_data(state->dates.reg, state->backup_register_dt);
|
||||
watch_store_backup_data(state->cycles.reg, state->backup_register_cy);
|
||||
state->period_today = !(state->period_today);
|
||||
beep(settings);
|
||||
}
|
||||
break;
|
||||
case first_period:
|
||||
// If tracking has not yet been activated
|
||||
if (!(state->dates.reg)) {
|
||||
unix_now = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), state->utc_offset);
|
||||
unix_prev_period = unix_now - (state->days_prev_period * SECONDS_PER_DAY);
|
||||
date_period = watch_utility_date_time_from_unix_time(unix_prev_period, state->utc_offset);
|
||||
state->dates.bit.first_day = date_period.unit.day;
|
||||
state->dates.bit.first_month = date_period.unit.month;
|
||||
state->dates.bit.first_year = date_period.unit.year;
|
||||
state->dates.bit.prev_day = date_period.unit.day;
|
||||
state->dates.bit.prev_month = date_period.unit.month;
|
||||
state->dates.bit.prev_year = date_period.unit.year;
|
||||
watch_store_backup_data(state->dates.reg, state->backup_register_dt);
|
||||
beep(settings);
|
||||
}
|
||||
break;
|
||||
case reset:
|
||||
if (state->reset_tracking) {
|
||||
reset_tracking(state);
|
||||
state->reset_tracking = !(state->reset_tracking);
|
||||
beep(settings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
switch (current_page) {
|
||||
case period_in_num_days:
|
||||
break;
|
||||
case average_cycle:
|
||||
break;
|
||||
case peak_fertility_window:
|
||||
break;
|
||||
case period_is_here:
|
||||
if (total_days_tracked(state))
|
||||
state->period_today = !(state->period_today);
|
||||
break;
|
||||
case first_period:
|
||||
if (!(state->dates.reg))
|
||||
state->days_prev_period = (state->days_prev_period > 99) ? 0 : state->days_prev_period + 1; // Cycle through pages to quickly reset to 0
|
||||
break;
|
||||
case reset:
|
||||
state->reset_tracking = !(state->reset_tracking);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
movement_move_to_face(0);
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
|
||||
watch_display_string((char *)menstrual_cycle_face_titles[current_page], 0);
|
||||
if (state->dates.reg)
|
||||
watch_set_indicator(WATCH_INDICATOR_SIGNAL); // signal that we are now in a tracking state
|
||||
|
||||
char buf[13];
|
||||
switch (current_page) {
|
||||
case period_in_num_days:
|
||||
sprintf(buf, "%2d", days_till_period(state));
|
||||
if (inside_fert_window(state))
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
watch_display_string(buf, 4);
|
||||
break;
|
||||
case average_cycle:
|
||||
sprintf(buf, "%2d", state->cycles.bit.average_cycle);
|
||||
watch_display_string(buf, 2);
|
||||
break;
|
||||
case peak_fertility_window:
|
||||
if (event.subsecond % 5 && state->dates.reg) { // blink active for 3 quarter-seconds
|
||||
first_day_fert = get_day_pk_fert(state, first_day);
|
||||
last_day_fert = get_day_pk_fert(state, last_day);
|
||||
sprintf(buf, "Fr%2d To %2d", first_day_fert, last_day_fert); // From: first day | To: last day
|
||||
if (inside_fert_window(state))
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
break;
|
||||
case period_is_here:
|
||||
if (event.subsecond % 5) { // blink active for 3 quarter-seconds
|
||||
if (!(state->dates.reg))
|
||||
watch_display_string("NA", 8); // Not Applicable: Do not allow period entry until tracking is activated...
|
||||
else if (state->period_today)
|
||||
watch_display_string("y", 9);
|
||||
else
|
||||
watch_display_string("n", 9);
|
||||
}
|
||||
break;
|
||||
case first_period:
|
||||
if (state->dates.reg) {
|
||||
if (!watch_tick_animation_is_running())
|
||||
watch_start_tick_animation(500); // Tracking activated
|
||||
}
|
||||
else if (event.subsecond % 5) { // blink active for 3 quarter-seconds
|
||||
sprintf(buf, "%2d", state->days_prev_period);
|
||||
watch_display_string(buf, 8);
|
||||
}
|
||||
break;
|
||||
case reset:
|
||||
// blink active for 3 quarter-seconds
|
||||
if (event.subsecond % 5 && state->reset_tracking)
|
||||
watch_display_string("y", 9);
|
||||
else if (event.subsecond % 5)
|
||||
watch_display_string("n", 9);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void menstrual_cycle_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
80
movement/watch_faces/complication/menstrual_cycle_face.h
Normal file
80
movement/watch_faces/complication/menstrual_cycle_face.h
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Joseph Borne Komosa | @jokomo24
|
||||
*
|
||||
* 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 MENSTRUAL_CYCLE_FACE_H_
|
||||
#define MENSTRUAL_CYCLE_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
typedef struct {
|
||||
// Store the date of the 'first' and the total cycles since to calulate and store the average menstrual cycle.
|
||||
// Store the date of the previous, most recent, period to calculate the cycle length.
|
||||
// Store the shortest and longest cycle to calculate the fertility window for The Calender Method.
|
||||
// NOTE: Not thrilled about using two registers, but could not find a way to perform The Calender Method
|
||||
// without requiring both the 'first' and 'prev' dates.
|
||||
union {
|
||||
struct {
|
||||
uint8_t first_day : 5;
|
||||
uint8_t first_month : 4;
|
||||
uint8_t first_year : 6; // 0-63 (representing 2020-2083)
|
||||
uint8_t prev_day : 5;
|
||||
uint8_t prev_month : 4;
|
||||
uint8_t prev_year : 6; // 0-63 (representing 2020-2083)
|
||||
uint8_t reserved : 2; // left over bit space
|
||||
} bit;
|
||||
uint32_t reg; // Tracking's been activated if > 0
|
||||
} dates;
|
||||
union {
|
||||
struct {
|
||||
uint8_t shortest_cycle : 6; // For step 2 of The Calender Method
|
||||
uint8_t longest_cycle : 6; // For step 3 of The Calender Method
|
||||
uint8_t average_cycle : 6; // The average menstrual cycle lasts 28 days, but normal cycles can vary from 21 to 35 days
|
||||
uint16_t total_cycles : 11; // The total cycles (periods) entered since the start of tracking
|
||||
uint8_t reserved : 3; // left over bit space
|
||||
} bit;
|
||||
uint32_t reg;
|
||||
} cycles;
|
||||
uint8_t backup_register_dt;
|
||||
uint8_t backup_register_cy;
|
||||
uint8_t current_page;
|
||||
uint8_t days_prev_period;
|
||||
int32_t utc_offset;
|
||||
bool period_today;
|
||||
bool reset_tracking;
|
||||
} menstrual_cycle_state_t;
|
||||
|
||||
void menstrual_cycle_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void menstrual_cycle_face_activate(movement_settings_t *settings, void *context);
|
||||
bool menstrual_cycle_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void menstrual_cycle_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define menstrual_cycle_face ((const watch_face_t){ \
|
||||
menstrual_cycle_face_setup, \
|
||||
menstrual_cycle_face_activate, \
|
||||
menstrual_cycle_face_loop, \
|
||||
menstrual_cycle_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // MENSTRUAL_CYCLE_FACE_H_
|
||||
263
movement/watch_faces/complication/metronome_face.c
Normal file
263
movement/watch_faces/complication/metronome_face.c
Normal file
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Austin Teets
|
||||
*
|
||||
* 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 "metronome_face.h"
|
||||
#include "watch.h"
|
||||
|
||||
static const int8_t _sound_seq_start[] = {BUZZER_NOTE_C8, 2, 0};
|
||||
static const int8_t _sound_seq_beat[] = {BUZZER_NOTE_C6, 2, 0};
|
||||
|
||||
void metronome_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
|
||||
(void) settings;
|
||||
(void) watch_face_index;
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(metronome_state_t));
|
||||
memset(*context_ptr, 0, sizeof(metronome_state_t));
|
||||
}
|
||||
}
|
||||
|
||||
void metronome_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
metronome_state_t *state = (metronome_state_t *)context;
|
||||
movement_request_tick_frequency(2);
|
||||
if (state->bpm == 0) {
|
||||
state->count = 4;
|
||||
state->bpm = 120;
|
||||
state->soundOn = true;
|
||||
}
|
||||
state->mode = metWait;
|
||||
state->correction = 0;
|
||||
state->setCur = hundred;
|
||||
}
|
||||
|
||||
static void _metronome_face_update_lcd(metronome_state_t *state) {
|
||||
char buf[11];
|
||||
if (state->soundOn) {
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
}
|
||||
sprintf(buf, "MN %d %03d%s", state->count, state->bpm, "bp");
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void _metronome_start_stop(metronome_state_t *state) {
|
||||
if (state->mode != metRun) {
|
||||
movement_request_tick_frequency(64);
|
||||
state->mode = metRun;
|
||||
watch_clear_display();
|
||||
double ticks = 3840.0 / (double)state->bpm;
|
||||
state->tick = (int) ticks;
|
||||
state->curTick = (int) ticks;
|
||||
state->halfBeat = (int)(state->tick/2);
|
||||
state->curCorrection = ticks - state->tick;
|
||||
state->correction = ticks - state->tick;
|
||||
state->curBeat = 1;
|
||||
} else {
|
||||
state->mode = metWait;
|
||||
movement_request_tick_frequency(2);
|
||||
_metronome_face_update_lcd(state);
|
||||
}
|
||||
}
|
||||
|
||||
static void _metronome_tick_beat(metronome_state_t *state) {
|
||||
char buf[11];
|
||||
if (state->soundOn) {
|
||||
if (state->curBeat == 1) {
|
||||
watch_buzzer_play_sequence((int8_t *)_sound_seq_start, NULL);
|
||||
} else {
|
||||
watch_buzzer_play_sequence((int8_t *)_sound_seq_beat, NULL);
|
||||
}
|
||||
}
|
||||
sprintf(buf, "MN %d %03d%s", state->count, state->bpm, "bp");
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void _metronome_event_tick(uint8_t subsecond, metronome_state_t *state) {
|
||||
(void) subsecond;
|
||||
|
||||
if (state->curCorrection >= 1) {
|
||||
state->curCorrection -= 1;
|
||||
state->curTick -= 1;
|
||||
}
|
||||
int diff = state->curTick - state->tick;
|
||||
if(diff == 0) {
|
||||
_metronome_tick_beat(state);
|
||||
state->curTick = 0;
|
||||
state->curCorrection += state->correction;
|
||||
if (state->curBeat < state->count ) {
|
||||
state->curBeat += 1;
|
||||
} else {
|
||||
state->curBeat = 1;
|
||||
}
|
||||
} else {
|
||||
if (state->curTick == state->halfBeat) {
|
||||
watch_clear_display();
|
||||
}
|
||||
state->curTick += 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void _metronome_setting_tick(uint8_t subsecond, metronome_state_t *state) {
|
||||
char buf[13];
|
||||
sprintf(buf, "MN %d %03d%s", state->count, state->bpm, "bp");
|
||||
if (subsecond%2 == 0) {
|
||||
switch (state->setCur) {
|
||||
case hundred:
|
||||
buf[5] = ' ';
|
||||
break;
|
||||
case ten:
|
||||
buf[6] = ' ';
|
||||
break;
|
||||
case one:
|
||||
buf[7] = ' ';
|
||||
break;
|
||||
case count:
|
||||
buf[3] = ' ';
|
||||
break;
|
||||
case alarm:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (state->setCur == alarm) {
|
||||
sprintf(buf, "MN 8eep%s", state->soundOn ? "On" : " -");
|
||||
}
|
||||
if (state->soundOn) {
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
}
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void _metronome_update_setting(metronome_state_t *state) {
|
||||
char buf[13];
|
||||
switch (state->setCur) {
|
||||
case hundred:
|
||||
if (state->bpm < 100) {
|
||||
state->bpm += 100;
|
||||
} else {
|
||||
state->bpm -= 100;
|
||||
}
|
||||
break;
|
||||
case ten:
|
||||
if ((state->bpm / 10) % 10 < 9) {
|
||||
state->bpm += 10;
|
||||
} else {
|
||||
state->bpm -= 90;
|
||||
}
|
||||
break;
|
||||
case one:
|
||||
if (state->bpm%10 < 9) {
|
||||
state->bpm += 1;
|
||||
} else {
|
||||
state->bpm -= 9;
|
||||
}
|
||||
break;
|
||||
case count:
|
||||
if (state->count < 9) {
|
||||
state->count += 1;
|
||||
} else {
|
||||
state->count = 2;
|
||||
}
|
||||
break;
|
||||
case alarm:
|
||||
state->soundOn = !state->soundOn;
|
||||
break;
|
||||
}
|
||||
sprintf(buf, "MN %d %03d%s", state->count % 10, state->bpm, "bp");
|
||||
if (state->setCur == alarm) {
|
||||
sprintf(buf, "MN 8eep%s", state->soundOn ? "On" : " -");
|
||||
}
|
||||
if (state->soundOn) {
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_BELL);
|
||||
}
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
bool metronome_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
metronome_state_t *state = (metronome_state_t *)context;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
_metronome_face_update_lcd(state);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
if (state->mode == metRun){
|
||||
_metronome_event_tick(event.subsecond, state);
|
||||
} else if (state->mode == setMenu) {
|
||||
_metronome_setting_tick(event.subsecond, state);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
if (state->mode == setMenu) {
|
||||
_metronome_update_setting(state);
|
||||
} else {
|
||||
_metronome_start_stop(state);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
if (state->mode == setMenu) {
|
||||
if (state->setCur < alarm) {
|
||||
state->setCur += 1;
|
||||
} else {
|
||||
state->setCur = hundred;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (state->mode != metRun && state->mode != setMenu) {
|
||||
movement_request_tick_frequency(2);
|
||||
state->mode = setMenu;
|
||||
_metronome_face_update_lcd(state);
|
||||
} else if (state->mode == setMenu) {
|
||||
state->mode = metWait;
|
||||
_metronome_face_update_lcd(state);
|
||||
}
|
||||
break;
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
movement_move_to_next_face();
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
if (state->mode != metRun) {
|
||||
movement_move_to_face(0);
|
||||
}
|
||||
break;
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void metronome_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
86
movement/watch_faces/complication/metronome_face.h
Normal file
86
movement/watch_faces/complication/metronome_face.h
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Austin Teets
|
||||
*
|
||||
* 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 METRONOME_FACE_H_
|
||||
#define METRONOME_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* A Metronome watch complication
|
||||
* Allows the user to set the BPM, counts per measure, beep sound on/off
|
||||
* Screen flashes on on the beat and off on the half beat (1/8th note)
|
||||
* Beep will sound high for downbeat and low for subsequent beats in measure
|
||||
* USE:
|
||||
* Press Alarm to start/stop metronome_face
|
||||
* Hold Alarm to enter settings menu
|
||||
* Short Light press will move through options
|
||||
* Short Alarm press will increment/toggle options
|
||||
* Long alarm press will exit options
|
||||
*/
|
||||
|
||||
typedef enum {
|
||||
metWait,
|
||||
metRun,
|
||||
setMenu
|
||||
} metronome_mode_t;
|
||||
|
||||
typedef enum {
|
||||
hundred,
|
||||
ten,
|
||||
one,
|
||||
count,
|
||||
alarm
|
||||
} setting_cursor_t;
|
||||
|
||||
typedef struct {
|
||||
// Anything you need to keep track of, put it here!
|
||||
uint8_t bpm;
|
||||
double correction;
|
||||
double curCorrection;
|
||||
int count;
|
||||
int tick;
|
||||
int curTick;
|
||||
int curBeat;
|
||||
int halfBeat;
|
||||
metronome_mode_t mode : 3;
|
||||
setting_cursor_t setCur : 4;
|
||||
bool soundOn;
|
||||
} metronome_state_t;
|
||||
|
||||
void metronome_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void metronome_face_activate(movement_settings_t *settings, void *context);
|
||||
bool metronome_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void metronome_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define metronome_face ((const watch_face_t){ \
|
||||
metronome_face_setup, \
|
||||
metronome_face_activate, \
|
||||
metronome_face_loop, \
|
||||
metronome_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // METRONOME_FACE_H_
|
||||
|
||||
504
movement/watch_faces/complication/smallchess_face.c
Normal file
504
movement/watch_faces/complication/smallchess_face.c
Normal file
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Jeremy O'Brien
|
||||
*
|
||||
* 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 "smallchesslib.h"
|
||||
|
||||
#include "smallchess_face.h"
|
||||
#include "watch.h"
|
||||
|
||||
#define PIECE_LIST_END_MARKER 0xff
|
||||
|
||||
int8_t cpu_done_beep[] = {BUZZER_NOTE_C5, 5, BUZZER_NOTE_C6, 5, BUZZER_NOTE_C7, 5, 0};
|
||||
|
||||
static void smallchess_init_board(smallchess_face_state_t *state) {
|
||||
SCL_gameInit((SCL_Game *)state->game, 0);
|
||||
memset(state->moveable_pieces, 0xff, sizeof(state->moveable_pieces));
|
||||
memset(state->moveable_dests, 0xff, sizeof(state->moveable_dests));
|
||||
}
|
||||
|
||||
void smallchess_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
|
||||
(void) settings;
|
||||
(void) watch_face_index;
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(smallchess_face_state_t));
|
||||
memset(*context_ptr, 0, sizeof(smallchess_face_state_t));
|
||||
|
||||
/* now alloc/init the game board */
|
||||
smallchess_face_state_t *state = (smallchess_face_state_t *)*context_ptr;
|
||||
state->game = malloc(sizeof(SCL_Game));
|
||||
smallchess_init_board(*context_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
void smallchess_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
static void _smallchess_calc_moveable_pieces(smallchess_face_state_t *state) {
|
||||
int moveable_pieces_idx = 0;
|
||||
SCL_Game *game = (SCL_Game *)state->game;
|
||||
for (int i = 0; i < SCL_BOARD_SQUARES; ++i) {
|
||||
if (game->board[i] != '.' &&
|
||||
SCL_pieceIsWhite(game->board[i]) == SCL_boardWhitesTurn(game->board)) {
|
||||
SCL_SquareSet moveable_pieces = SCL_SQUARE_SET_EMPTY;
|
||||
SCL_boardGetMoves(game->board, i, moveable_pieces);
|
||||
if (SCL_squareSetSize(moveable_pieces) != 0) {
|
||||
state->moveable_pieces[moveable_pieces_idx] = i;
|
||||
moveable_pieces_idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
state->moveable_pieces[moveable_pieces_idx] = PIECE_LIST_END_MARKER;
|
||||
state->moveable_pieces_idx = 0;
|
||||
}
|
||||
|
||||
static void _smallchess_make_ai_move(smallchess_face_state_t *state) {
|
||||
char ai_from_str[3] = {0};
|
||||
char ai_to_str[3] = {0};
|
||||
uint8_t rep_from, rep_to;
|
||||
char ai_prom;
|
||||
|
||||
watch_clear_display();
|
||||
watch_start_character_blink('C', 100);
|
||||
SCL_gameGetRepetiotionMove(state->game, &rep_from, &rep_to);
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
hri_oscctrl_write_OSC16MCTRL_FSEL_bf(OSCCTRL, OSCCTRL_OSC16MCTRL_FSEL_16_Val);
|
||||
#endif
|
||||
SCL_getAIMove(state->game, 3, 0, 0, SCL_boardEvaluateStatic, NULL, 0, rep_from, rep_to, &state->ai_from_square, &state->ai_to_square, &ai_prom);
|
||||
#ifndef __EMSCRIPTEN__
|
||||
hri_oscctrl_write_OSC16MCTRL_FSEL_bf(OSCCTRL, OSCCTRL_OSC16MCTRL_FSEL_4_Val);
|
||||
#endif
|
||||
|
||||
SCL_gameMakeMove(state->game, state->ai_from_square, state->ai_to_square, ai_prom);
|
||||
watch_stop_blink();
|
||||
|
||||
watch_buzzer_play_sequence(cpu_done_beep, NULL);
|
||||
|
||||
/* cache the move as a string for SHOW_CPU_MOVE state */
|
||||
SCL_squareToString(state->ai_from_square, ai_from_str);
|
||||
SCL_squareToString(state->ai_to_square, ai_to_str);
|
||||
snprintf(state->last_move_str, sizeof(state->last_move_str), " %s-%s", ai_from_str, ai_to_str);
|
||||
|
||||
/* now cache the list of legal pieces we can move */
|
||||
_smallchess_calc_moveable_pieces(state);
|
||||
}
|
||||
|
||||
static char _smallchess_make_lowercase(char c) {
|
||||
if (c < 0x61)
|
||||
return c + 0x20;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
static void _smallchess_get_endgame_string(smallchess_face_state_t *state, char *buf, uint8_t len) {
|
||||
uint8_t endgame_state = ((SCL_Game *)state->game)->state;
|
||||
uint16_t ply = ((SCL_Game *)state->game)->ply;
|
||||
|
||||
switch (endgame_state) {
|
||||
case SCL_GAME_STATE_WHITE_WIN:
|
||||
snprintf(buf, len, "Wh%2dm&ate ", ply);
|
||||
break;
|
||||
case SCL_GAME_STATE_BLACK_WIN:
|
||||
snprintf(buf, len, "bL%2dm&ate ", ply);
|
||||
break;
|
||||
case SCL_GAME_STATE_DRAW:
|
||||
case SCL_GAME_STATE_DRAW_STALEMATE:
|
||||
case SCL_GAME_STATE_DRAW_REPETITION:
|
||||
case SCL_GAME_STATE_DRAW_50:
|
||||
case SCL_GAME_STATE_DRAW_DEAD:
|
||||
snprintf(buf, len, " %2d Drauu", ply);
|
||||
break;
|
||||
default:
|
||||
snprintf(buf, len, " %2d Error", ply);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_face_update_lcd(smallchess_face_state_t *state) {
|
||||
uint8_t start_square;
|
||||
uint8_t end_square;
|
||||
char start_coord[3] = {0};
|
||||
char end_coord[3] = {0};
|
||||
char buf[14] = {0};
|
||||
|
||||
uint16_t ply = ((SCL_Game *)state->game)->ply;
|
||||
|
||||
switch (state->state) {
|
||||
case SMALLCHESS_MENU_RESUME:
|
||||
snprintf(buf, sizeof(buf), "SC%2dResume", ply);
|
||||
break;
|
||||
case SMALLCHESS_MENU_UNDO:
|
||||
snprintf(buf, sizeof(buf), "SC%2d Undo ", ply);
|
||||
break;
|
||||
case SMALLCHESS_MENU_SHOW_LAST_MOVE:
|
||||
snprintf(buf, sizeof(buf), "SC%2dShLast", ply);
|
||||
break;
|
||||
case SMALLCHESS_MENU_NEW_WHITE:
|
||||
snprintf(buf, sizeof(buf), "Wh%2dStart ", ply);
|
||||
break;
|
||||
case SMALLCHESS_MENU_NEW_BLACK:
|
||||
snprintf(buf, sizeof(buf), "bL%2dStart ", ply);
|
||||
break;
|
||||
case SMALLCHESS_SHOW_CPU_MOVE:
|
||||
case SMALLCHESS_SHOW_LAST_MOVE:
|
||||
snprintf(buf,
|
||||
sizeof(buf),
|
||||
"%c %2d%s",
|
||||
_smallchess_make_lowercase(((SCL_Game *)state->game)->board[state->ai_to_square]),
|
||||
ply,
|
||||
state->last_move_str);
|
||||
|
||||
break;
|
||||
case SMALLCHESS_SELECT_PIECE:
|
||||
if (((SCL_Game *)state->game)->state != SCL_GAME_STATE_PLAYING) {
|
||||
_smallchess_get_endgame_string(state, buf, sizeof(buf));
|
||||
break;
|
||||
}
|
||||
start_square = state->moveable_pieces[state->moveable_pieces_idx];
|
||||
SCL_squareToString(start_square, start_coord);
|
||||
snprintf(buf,
|
||||
sizeof(buf),
|
||||
"%c %2d %s- ",
|
||||
_smallchess_make_lowercase(((SCL_Game *)state->game)->board[start_square]),
|
||||
ply + 1,
|
||||
start_coord);
|
||||
break;
|
||||
case SMALLCHESS_SELECT_DEST:
|
||||
start_square = state->moveable_pieces[state->moveable_pieces_idx];
|
||||
SCL_squareToString(start_square, start_coord);
|
||||
end_square = state->moveable_dests[state->moveable_dests_idx];
|
||||
SCL_squareToString(end_square, end_coord);
|
||||
snprintf(buf,
|
||||
sizeof(buf),
|
||||
"%c %2d %s-%s",
|
||||
_smallchess_make_lowercase(((SCL_Game *)state->game)->board[start_square]),
|
||||
ply + 1,
|
||||
start_coord,
|
||||
end_coord);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void _smallchess_select_main_menu_subitem(smallchess_face_state_t *state) {
|
||||
char from_str[3] = {0};
|
||||
char to_str[3] = {0};
|
||||
char prom;
|
||||
|
||||
switch (state->state) {
|
||||
case SMALLCHESS_MENU_RESUME:
|
||||
state->state = SMALLCHESS_SELECT_PIECE;
|
||||
break;
|
||||
case SMALLCHESS_MENU_UNDO:
|
||||
/* undo twice to undo the CPU's move and our move */
|
||||
SCL_gameUndoMove((SCL_Game *)state->game);
|
||||
SCL_gameUndoMove((SCL_Game *)state->game);
|
||||
/* and re-calculate the moveable pieces for this new state */
|
||||
_smallchess_calc_moveable_pieces(state);
|
||||
break;
|
||||
case SMALLCHESS_MENU_NEW_WHITE:
|
||||
SCL_gameInit((SCL_Game *)state->game, 0);
|
||||
_smallchess_calc_moveable_pieces(state);
|
||||
state->state = SMALLCHESS_SELECT_PIECE;
|
||||
break;
|
||||
case SMALLCHESS_MENU_NEW_BLACK:
|
||||
SCL_gameInit((SCL_Game *)state->game, 0);
|
||||
/* force a move since black is playing */
|
||||
_smallchess_make_ai_move(state);
|
||||
state->state = SMALLCHESS_SHOW_CPU_MOVE;
|
||||
break;
|
||||
case SMALLCHESS_MENU_SHOW_LAST_MOVE:
|
||||
/* fetch the move */
|
||||
SCL_recordGetMove(((SCL_Game *)state->game)->record, ((SCL_Game *)state->game)->ply - 1, &state->ai_from_square, &state->ai_to_square, &prom);
|
||||
SCL_squareToString(state->ai_from_square, from_str);
|
||||
SCL_squareToString(state->ai_to_square, to_str);
|
||||
snprintf(state->last_move_str, sizeof(state->last_move_str), " %s-%s", from_str, to_str);
|
||||
state->state = SMALLCHESS_SHOW_LAST_MOVE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_handle_select_piece_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
SCL_SquareSet moveable_dests = SCL_SQUARE_SET_EMPTY;
|
||||
|
||||
/* back to main menu on any event when game ends */
|
||||
if (((SCL_Game *)state->game)->state != SCL_GAME_STATE_PLAYING) {
|
||||
state->state = SMALLCHESS_MENU_RESUME;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
// check for no moves possible state (shouldn't happen but this will prevent weirdness)
|
||||
if (state->moveable_pieces[0] == PIECE_LIST_END_MARKER) {
|
||||
return;
|
||||
}
|
||||
|
||||
state->moveable_pieces_idx += 1;
|
||||
if (state->moveable_pieces_idx >= NUM_ELEMENTS(state->moveable_pieces)) {
|
||||
state->moveable_pieces_idx = 0;
|
||||
}
|
||||
|
||||
if (state->moveable_pieces[state->moveable_pieces_idx] == PIECE_LIST_END_MARKER) {
|
||||
state->moveable_pieces_idx = 0;
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
// check for no moves possible state (shouldn't happen but this will prevent weirdness)
|
||||
if (state->moveable_pieces[0] == PIECE_LIST_END_MARKER) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* handle wrap around */
|
||||
if (state->moveable_pieces_idx == 0) {
|
||||
for (unsigned int i = 0; i < NUM_ELEMENTS(state->moveable_pieces); i++) {
|
||||
if (state->moveable_pieces[i] == 0xff) {
|
||||
state->moveable_pieces_idx = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state->moveable_pieces_idx -= 1;
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
if (((SCL_Game *)state->game)->ply == 0) {
|
||||
state->state = SMALLCHESS_MENU_NEW_WHITE;
|
||||
} else {
|
||||
state->state = SMALLCHESS_MENU_RESUME;
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
/* pre-calculate the possible moves this piece can make */
|
||||
SCL_boardGetMoves(((SCL_Game *)state->game)->board, state->moveable_pieces[state->moveable_pieces_idx], moveable_dests);
|
||||
state->moveable_dests_idx = 0;
|
||||
SCL_SQUARE_SET_ITERATE_BEGIN(moveable_dests)
|
||||
state->moveable_dests[state->moveable_dests_idx] = iteratedSquare;
|
||||
state->moveable_dests_idx++;
|
||||
SCL_SQUARE_SET_ITERATE_END
|
||||
state->moveable_dests[state->moveable_dests_idx] = PIECE_LIST_END_MARKER;
|
||||
state->moveable_dests_idx = 0;
|
||||
state->state = SMALLCHESS_SELECT_DEST;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_handle_select_dest_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
// check for no moves possible state (shouldn't happen but this will prevent weirdness)
|
||||
if (state->moveable_dests[0] == PIECE_LIST_END_MARKER) {
|
||||
return;
|
||||
}
|
||||
state->moveable_dests_idx += 1;
|
||||
if (state->moveable_dests_idx >= (sizeof(state->moveable_dests) / sizeof(state->moveable_dests[0]))) {
|
||||
state->moveable_dests_idx = 0;
|
||||
}
|
||||
|
||||
if (state->moveable_dests[state->moveable_dests_idx] == PIECE_LIST_END_MARKER) {
|
||||
state->moveable_dests_idx = 0;
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
// check for no moves possible state (shouldn't happen but this will prevent weirdness)
|
||||
if (state->moveable_dests[0] == PIECE_LIST_END_MARKER) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* handle wrap around */
|
||||
if (state->moveable_dests_idx == 0) {
|
||||
for (unsigned int i = 0; i < NUM_ELEMENTS(state->moveable_dests); i++) {
|
||||
if (state->moveable_dests[i] == 0xff) {
|
||||
state->moveable_dests_idx = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state->moveable_dests_idx -= 1;
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
state->state = SMALLCHESS_SELECT_PIECE;
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
SCL_gameMakeMove((SCL_Game *)state->game, state->moveable_pieces[state->moveable_pieces_idx], state->moveable_dests[state->moveable_dests_idx], 'q');
|
||||
|
||||
/* if the player didn't win or draw here, calculate a move */
|
||||
if (((SCL_Game *)state->game)->state == SCL_GAME_STATE_PLAYING) {
|
||||
_smallchess_make_ai_move(state);
|
||||
state->state = SMALLCHESS_SHOW_CPU_MOVE;
|
||||
} else {
|
||||
/* player ended the game through mate or draw; jump to select piece screen to show state */
|
||||
state->state = SMALLCHESS_SELECT_PIECE;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* this just waits until any button is hit */
|
||||
static void _smallchess_handle_show_cpu_move_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
state->state = SMALLCHESS_SELECT_PIECE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_handle_show_last_move_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
state->state = SMALLCHESS_MENU_SHOW_LAST_MOVE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_handle_playing_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
if (state->state == SMALLCHESS_SELECT_PIECE) {
|
||||
_smallchess_handle_select_piece_button_event(state, event);
|
||||
} else if (state->state == SMALLCHESS_SELECT_DEST) {
|
||||
_smallchess_handle_select_dest_button_event(state, event);
|
||||
} else if (state->state == SMALLCHESS_SHOW_CPU_MOVE) {
|
||||
_smallchess_handle_show_cpu_move_button_event(state, event);
|
||||
} else if (state->state == SMALLCHESS_SHOW_LAST_MOVE) {
|
||||
_smallchess_handle_show_last_move_button_event(state, event);
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_handle_main_menu_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
uint16_t ply = ((SCL_Game *)state->game)->ply;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
/* no game started; only offer start white/start black */
|
||||
if (ply == 0) {
|
||||
if (state->state == SMALLCHESS_MENU_NEW_WHITE) {
|
||||
state->state = SMALLCHESS_MENU_NEW_BLACK;
|
||||
} else {
|
||||
state->state = SMALLCHESS_MENU_NEW_WHITE;
|
||||
}
|
||||
} else {
|
||||
state->state++;
|
||||
if (state->state >= SMALLCHESS_PLAYING_SPLIT) {
|
||||
state->state = SMALLCHESS_MENU_RESUME;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
/* no game started; only offer start white/start black */
|
||||
if (ply == 0) {
|
||||
if (state->state == SMALLCHESS_MENU_NEW_BLACK) {
|
||||
state->state = SMALLCHESS_MENU_NEW_WHITE;
|
||||
} else {
|
||||
state->state = SMALLCHESS_MENU_NEW_BLACK;
|
||||
}
|
||||
} else {
|
||||
if (state->state == SMALLCHESS_MENU_RESUME) {
|
||||
state->state = SMALLCHESS_PLAYING_SPLIT - 1;
|
||||
} else {
|
||||
state->state--;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
_smallchess_select_main_menu_subitem(state);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _smallchess_handle_button_event(smallchess_face_state_t *state, movement_event_t event) {
|
||||
if (state->state < SMALLCHESS_PLAYING_SPLIT) {
|
||||
/* in main menu */
|
||||
_smallchess_handle_main_menu_button_event(state, event);
|
||||
} else if (state->state > SMALLCHESS_PLAYING_SPLIT) {
|
||||
/* in piece selection */
|
||||
_smallchess_handle_playing_button_event(state, event);
|
||||
}
|
||||
}
|
||||
|
||||
bool smallchess_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
smallchess_face_state_t *state = (smallchess_face_state_t *)context;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
if (((SCL_Game *)state->game)->ply == 0) {
|
||||
state->state = SMALLCHESS_MENU_NEW_WHITE;
|
||||
} else {
|
||||
state->state = SMALLCHESS_MENU_RESUME;
|
||||
}
|
||||
_smallchess_face_update_lcd(state);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
_smallchess_handle_button_event(state, event);
|
||||
_smallchess_face_update_lcd(state);
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
break;
|
||||
default:
|
||||
movement_default_loop_handler(event, settings);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void smallchess_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
watch_set_led_off();
|
||||
}
|
||||
90
movement/watch_faces/complication/smallchess_face.h
Normal file
90
movement/watch_faces/complication/smallchess_face.h
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Jeremy O'Brien
|
||||
*
|
||||
* 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 SMALLCHESS_FACE_H_
|
||||
#define SMALLCHESS_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* Chess watchface
|
||||
*
|
||||
* Implements a (very) simple chess engine.
|
||||
* Uses smallchesslib for the engine: https://codeberg.org/drummyfish/smallchesslib
|
||||
*
|
||||
* When moving a piece, only valid pieces and moves are presented.
|
||||
*
|
||||
* Interaction is done through a simple menu/submenu system:
|
||||
* - Light button: navigate backwards through the current menu
|
||||
* - Alarm button: navigate forwards through the current menu
|
||||
* - Light button (long press): navigate up to the parent menu
|
||||
* - Alarm button (long press): select the current item or submenu
|
||||
*/
|
||||
|
||||
enum smallchess_state {
|
||||
/* main menu */
|
||||
SMALLCHESS_MENU_RESUME,
|
||||
SMALLCHESS_MENU_SHOW_LAST_MOVE,
|
||||
SMALLCHESS_MENU_UNDO,
|
||||
SMALLCHESS_MENU_NEW_WHITE,
|
||||
SMALLCHESS_MENU_NEW_BLACK,
|
||||
|
||||
SMALLCHESS_PLAYING_SPLIT,
|
||||
|
||||
/* playing game submenu */
|
||||
SMALLCHESS_SHOW_LAST_MOVE,
|
||||
SMALLCHESS_SHOW_CPU_MOVE,
|
||||
SMALLCHESS_SELECT_PIECE,
|
||||
SMALLCHESS_SELECT_DEST,
|
||||
};
|
||||
|
||||
#define NUM_ELEMENTS(a) (sizeof(a) / sizeof(a[0]))
|
||||
#define SMALLCHESS_NUM_PIECES 16 // number of pieces each player has
|
||||
|
||||
typedef struct {
|
||||
void *game;
|
||||
enum smallchess_state state;
|
||||
uint8_t moveable_pieces[SMALLCHESS_NUM_PIECES + 1];
|
||||
uint8_t moveable_pieces_idx;
|
||||
uint8_t moveable_dests[29]; // this magic number represents the maximum number of moves a piece can make (queen in center of board)
|
||||
// plus one for the end list marker
|
||||
uint8_t moveable_dests_idx;
|
||||
char last_move_str[7];
|
||||
uint8_t ai_from_square, ai_to_square;
|
||||
} smallchess_face_state_t;
|
||||
|
||||
void smallchess_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void smallchess_face_activate(movement_settings_t *settings, void *context);
|
||||
bool smallchess_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void smallchess_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define smallchess_face ((const watch_face_t){ \
|
||||
smallchess_face_setup, \
|
||||
smallchess_face_activate, \
|
||||
smallchess_face_loop, \
|
||||
smallchess_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // SMALLCHESS_FACE_H_
|
||||
482
movement/watch_faces/complication/sunrise_sunset_alt_face.c
Normal file
482
movement/watch_faces/complication/sunrise_sunset_alt_face.c
Normal file
@@ -0,0 +1,482 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Joey Castillo
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Sunrise/sunset calculations are public domain code by Paul Schlyter, December 1992
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include "sunrise_sunset_alt_face.h"
|
||||
#include "watch.h"
|
||||
#include "watch_utility.h"
|
||||
#include "sunriset.h"
|
||||
|
||||
#if __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
|
||||
static const uint8_t _location_count = sizeof(alt_longLatPresets) / sizeof(alt_long_lat_presets_t);
|
||||
|
||||
static int compare(const void *a, const void *b) {
|
||||
SolarEvent *eventA = (SolarEvent *)a;
|
||||
SolarEvent *eventB = (SolarEvent *)b;
|
||||
|
||||
return (eventA->timestamp - eventB->timestamp);
|
||||
}
|
||||
|
||||
static void set_sunriset(double time_value, watch_date_time *time_unit) {
|
||||
// Calculate minutes and seconds
|
||||
float minutes = 60.0 * fmod(time_value, 1);
|
||||
float seconds = 60.0 * fmod(minutes, 1);
|
||||
|
||||
// Set hour and minute
|
||||
time_unit->unit.hour = floor(time_value);
|
||||
if (seconds < 30) {
|
||||
time_unit->unit.minute = floor(minutes);
|
||||
} else {
|
||||
time_unit->unit.minute = ceil(minutes);
|
||||
}
|
||||
|
||||
// Handle edge case where minutes equal 60
|
||||
if (time_unit->unit.minute == 60) {
|
||||
time_unit->unit.minute = 0;
|
||||
time_unit->unit.hour = (time_unit->unit.hour + 1) % 24;
|
||||
}
|
||||
}
|
||||
|
||||
static void check_and_update_sunriset(double event_type, watch_date_time *event_time, watch_date_time utc_now, watch_date_time date_time) {
|
||||
set_sunriset(event_type, event_time);
|
||||
if (date_time.reg > event_time->reg) {
|
||||
// It's after the specific solar event. We need to display the event time for tomorrow.
|
||||
uint32_t timestamp = watch_utility_date_time_to_unix_time(utc_now, 0);
|
||||
timestamp += 86400; // Advance by 24 hours
|
||||
*event_time = watch_utility_date_time_from_unix_time(timestamp, 0);
|
||||
set_sunriset(event_type, event_time); // Update for the next day
|
||||
}
|
||||
}
|
||||
|
||||
static void display_time(watch_date_time *time, const char *prefix, movement_settings_t *settings, sunrise_sunset_alt_state_t *state) {
|
||||
bool set_leading_zero = false;
|
||||
char buf[14];
|
||||
|
||||
watch_set_colon();
|
||||
// Handle 12-hour mode and PM indicator
|
||||
if (!settings->bit.clock_mode_24h) {
|
||||
if (watch_utility_convert_to_12_hour(time)) {
|
||||
watch_set_indicator(WATCH_INDICATOR_PM);
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
}
|
||||
}
|
||||
// Handle 24-hour mode with leading zero
|
||||
else if (settings->bit.clock_24h_leading_zero && time->unit.hour < 10) {
|
||||
set_leading_zero = true;
|
||||
}
|
||||
|
||||
// Format and display time
|
||||
sprintf(buf, "%s%2d%2d%02d%s", prefix, time->unit.day, time->unit.hour, time->unit.minute, alt_longLatPresets[state->longLatToUse].name);
|
||||
watch_display_string(buf, 0);
|
||||
|
||||
// Display leading zero if needed
|
||||
if (set_leading_zero) {
|
||||
watch_display_string("0", 4);
|
||||
}
|
||||
}
|
||||
|
||||
static void sunrise_sunset_alt_face_update(movement_settings_t *settings, sunrise_sunset_alt_state_t *state) {
|
||||
double naut_start, civ_start, rise, set, civ_end, naut_end;
|
||||
movement_location_t movement_location;
|
||||
if (state->longLatToUse == 0 || _location_count <= 1)
|
||||
movement_location = (movement_location_t) watch_get_backup_data(1);
|
||||
else{
|
||||
movement_location.bit.latitude = alt_longLatPresets[state->longLatToUse].latitude;
|
||||
movement_location.bit.longitude = alt_longLatPresets[state->longLatToUse].longitude;
|
||||
}
|
||||
|
||||
if (movement_location.reg == 0) {
|
||||
watch_clear_colon();
|
||||
watch_clear_indicator(WATCH_INDICATOR_PM);
|
||||
watch_display_string("RI no Loc", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
watch_date_time date_time = watch_rtc_get_date_time(); // the current local date / time
|
||||
watch_date_time utc_now = watch_utility_date_time_convert_zone(date_time, movement_timezone_offsets[settings->bit.time_zone] * 60, 0); // the current date / time in UTC
|
||||
|
||||
watch_date_time scratch_time; // scratchpad, contains different values at different times
|
||||
watch_date_time naut_start_time;
|
||||
watch_date_time civ_start_time;
|
||||
watch_date_time rise_time;
|
||||
watch_date_time set_time;
|
||||
watch_date_time civ_end_time;
|
||||
watch_date_time naut_end_time;
|
||||
|
||||
scratch_time.reg = utc_now.reg;
|
||||
naut_start_time.reg = utc_now.reg;
|
||||
civ_start_time.reg = utc_now.reg;
|
||||
rise_time.reg = utc_now.reg;
|
||||
set_time.reg = utc_now.reg;
|
||||
civ_end_time.reg = utc_now.reg;
|
||||
naut_end_time.reg = utc_now.reg;
|
||||
|
||||
// Weird quirky unsigned things were happening when I tried to cast these directly to doubles below.
|
||||
// it looks redundant, but extracting them to local int16's seemed to fix it.
|
||||
int16_t lat_centi = (int16_t)movement_location.bit.latitude;
|
||||
int16_t lon_centi = (int16_t)movement_location.bit.longitude;
|
||||
|
||||
double lat = (double)lat_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 struct.
|
||||
// to deal with this, we set aside the offset in hours, and add it back before converting it to a watch_date_time.
|
||||
double hours_from_utc = ((double)movement_timezone_offsets[settings->bit.time_zone]) / 60.0;
|
||||
|
||||
sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &rise, &set);
|
||||
civil_twilight(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &civ_start, &civ_end);
|
||||
nautical_twilight(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &naut_start, &naut_end);
|
||||
|
||||
if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H);
|
||||
|
||||
naut_start += hours_from_utc;
|
||||
civ_start += hours_from_utc;
|
||||
rise += hours_from_utc;
|
||||
set += hours_from_utc;
|
||||
civ_end += hours_from_utc;
|
||||
naut_end += hours_from_utc;
|
||||
|
||||
check_and_update_sunriset(naut_start, &naut_start_time, utc_now, date_time);
|
||||
check_and_update_sunriset(civ_start, &civ_start_time, utc_now, date_time);
|
||||
check_and_update_sunriset(rise, &rise_time, utc_now, date_time);
|
||||
check_and_update_sunriset(set, &set_time, utc_now, date_time);
|
||||
check_and_update_sunriset(civ_end, &civ_end_time, utc_now, date_time);
|
||||
check_and_update_sunriset(naut_end, &naut_end_time, utc_now, date_time);
|
||||
|
||||
SolarEvent events[] = {
|
||||
{naut_start_time.reg, &naut_start_time, "nt"},
|
||||
{civ_start_time.reg, &civ_start_time, "cI"},
|
||||
{rise_time.reg, &rise_time, "rI"},
|
||||
{set_time.reg, &set_time, "SE"},
|
||||
{civ_end_time.reg, &civ_end_time, "cI"},
|
||||
{naut_end_time.reg, &naut_end_time, "nt"}
|
||||
};
|
||||
|
||||
uint8_t n = sizeof(events) / sizeof(events[0]);
|
||||
|
||||
// Sort the array of events
|
||||
qsort(events, n, sizeof(SolarEvent), compare);
|
||||
|
||||
switch (state->rise_index) {
|
||||
case 0:
|
||||
display_time(events[0].event, events[0].abreviation, settings, state);
|
||||
break;
|
||||
case 1:
|
||||
display_time(events[1].event, events[1].abreviation, settings, state);
|
||||
break;
|
||||
case 2:
|
||||
display_time(events[2].event, events[2].abreviation, settings, state);
|
||||
break;
|
||||
case 3:
|
||||
display_time(events[3].event, events[3].abreviation, settings, state);
|
||||
break;
|
||||
case 4:
|
||||
display_time(events[4].event, events[4].abreviation, settings, state);
|
||||
break;
|
||||
case 5:
|
||||
display_time(events[5].event, events[5].abreviation, settings, state);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static int16_t sunrise_sunset_alt_face_latlon_from_struct(sunrise_sunset_alt_lat_lon_settings_t val) {
|
||||
int16_t retval = (val.sign ? -1 : 1) *
|
||||
(
|
||||
val.hundreds * 10000 +
|
||||
val.tens * 1000 +
|
||||
val.ones * 100 +
|
||||
val.tenths * 10 +
|
||||
val.hundredths
|
||||
);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static sunrise_sunset_alt_lat_lon_settings_t sunrise_sunset_alt_face_struct_from_latlon(int16_t val) {
|
||||
sunrise_sunset_alt_lat_lon_settings_t retval;
|
||||
|
||||
retval.sign = val < 0;
|
||||
val = abs(val);
|
||||
retval.hundredths = val % 10;
|
||||
val /= 10;
|
||||
retval.tenths = val % 10;
|
||||
val /= 10;
|
||||
retval.ones = val % 10;
|
||||
val /= 10;
|
||||
retval.tens = val % 10;
|
||||
val /= 10;
|
||||
retval.hundreds = val % 10;
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
static void sunrise_sunset_alt_face_update_location_register(sunrise_sunset_alt_state_t *state) {
|
||||
if (state->location_changed) {
|
||||
movement_location_t movement_location;
|
||||
int16_t lat = sunrise_sunset_alt_face_latlon_from_struct(state->working_latitude);
|
||||
int16_t lon = sunrise_sunset_alt_face_latlon_from_struct(state->working_longitude);
|
||||
movement_location.bit.latitude = lat;
|
||||
movement_location.bit.longitude = lon;
|
||||
watch_store_backup_data(movement_location.reg, 1);
|
||||
state->location_changed = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void sunrise_sunset_alt_face_update_settings_display(movement_event_t event, sunrise_sunset_alt_state_t *state) {
|
||||
char buf[12];
|
||||
|
||||
switch (state->page) {
|
||||
case 0:
|
||||
return;
|
||||
case 1:
|
||||
sprintf(buf, "LA %c %04d", state->working_latitude.sign ? '-' : '+', abs(sunrise_sunset_alt_face_latlon_from_struct(state->working_latitude)));
|
||||
break;
|
||||
case 2:
|
||||
sprintf(buf, "LO %c%05d", state->working_longitude.sign ? '-' : '+', abs(sunrise_sunset_alt_face_latlon_from_struct(state->working_longitude)));
|
||||
break;
|
||||
}
|
||||
if (event.subsecond % 2) {
|
||||
buf[state->active_digit + 4] = ' ';
|
||||
}
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
static void sunrise_sunset_alt_face_advance_digit(sunrise_sunset_alt_state_t *state) {
|
||||
state->location_changed = true;
|
||||
switch (state->page) {
|
||||
case 1: // latitude
|
||||
switch (state->active_digit) {
|
||||
case 0:
|
||||
state->working_latitude.sign++;
|
||||
break;
|
||||
case 1:
|
||||
// we skip this digit
|
||||
break;
|
||||
case 2:
|
||||
state->working_latitude.tens = (state->working_latitude.tens + 1) % 10;
|
||||
if (abs(sunrise_sunset_alt_face_latlon_from_struct(state->working_latitude)) > 9000) {
|
||||
// prevent latitude from going over ±90.
|
||||
// TODO: perform these checks when advancing the digit?
|
||||
state->working_latitude.ones = 0;
|
||||
state->working_latitude.tenths = 0;
|
||||
state->working_latitude.hundredths = 0;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
state->working_latitude.ones = (state->working_latitude.ones + 1) % 10;
|
||||
if (abs(sunrise_sunset_alt_face_latlon_from_struct(state->working_latitude)) > 9000) state->working_latitude.ones = 0;
|
||||
break;
|
||||
case 4:
|
||||
state->working_latitude.tenths = (state->working_latitude.tenths + 1) % 10;
|
||||
if (abs(sunrise_sunset_alt_face_latlon_from_struct(state->working_latitude)) > 9000) state->working_latitude.tenths = 0;
|
||||
break;
|
||||
case 5:
|
||||
state->working_latitude.hundredths = (state->working_latitude.hundredths + 1) % 10;
|
||||
if (abs(sunrise_sunset_alt_face_latlon_from_struct(state->working_latitude)) > 9000) state->working_latitude.hundredths = 0;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 2: // longitude
|
||||
switch (state->active_digit) {
|
||||
case 0:
|
||||
state->working_longitude.sign++;
|
||||
break;
|
||||
case 1:
|
||||
state->working_longitude.hundreds = (state->working_longitude.hundreds + 1) % 10;
|
||||
if (abs(sunrise_sunset_alt_face_latlon_from_struct(state->working_longitude)) > 18000) {
|
||||
// prevent longitude from going over ±180
|
||||
state->working_longitude.tens = 8;
|
||||
state->working_longitude.ones = 0;
|
||||
state->working_longitude.tenths = 0;
|
||||
state->working_longitude.hundredths = 0;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
state->working_longitude.tens = (state->working_longitude.tens + 1) % 10;
|
||||
if (abs(sunrise_sunset_alt_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.tens = 0;
|
||||
break;
|
||||
case 3:
|
||||
state->working_longitude.ones = (state->working_longitude.ones + 1) % 10;
|
||||
if (abs(sunrise_sunset_alt_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.ones = 0;
|
||||
break;
|
||||
case 4:
|
||||
state->working_longitude.tenths = (state->working_longitude.tenths + 1) % 10;
|
||||
if (abs(sunrise_sunset_alt_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.tenths = 0;
|
||||
break;
|
||||
case 5:
|
||||
state->working_longitude.hundredths = (state->working_longitude.hundredths + 1) % 10;
|
||||
if (abs(sunrise_sunset_alt_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.hundredths = 0;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void sunrise_sunset_alt_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
|
||||
(void) settings;
|
||||
(void) watch_face_index;
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(sunrise_sunset_alt_state_t));
|
||||
memset(*context_ptr, 0, sizeof(sunrise_sunset_alt_state_t));
|
||||
}
|
||||
}
|
||||
|
||||
void sunrise_sunset_alt_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
if (watch_tick_animation_is_running()) watch_stop_tick_animation();
|
||||
|
||||
#if __EMSCRIPTEN__
|
||||
int16_t browser_lat = EM_ASM_INT({
|
||||
return lat;
|
||||
});
|
||||
int16_t browser_lon = EM_ASM_INT({
|
||||
return lon;
|
||||
});
|
||||
if ((watch_get_backup_data(1) == 0) && (browser_lat || browser_lon)) {
|
||||
movement_location_t browser_loc;
|
||||
browser_loc.bit.latitude = browser_lat;
|
||||
browser_loc.bit.longitude = browser_lon;
|
||||
watch_store_backup_data(browser_loc.reg, 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
sunrise_sunset_alt_state_t *state = (sunrise_sunset_alt_state_t *)context;
|
||||
movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1);
|
||||
state->working_latitude = sunrise_sunset_alt_face_struct_from_latlon(movement_location.bit.latitude);
|
||||
state->working_longitude = sunrise_sunset_alt_face_struct_from_latlon(movement_location.bit.longitude);
|
||||
}
|
||||
|
||||
bool sunrise_sunset_alt_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
sunrise_sunset_alt_state_t *state = (sunrise_sunset_alt_state_t *)context;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
sunrise_sunset_alt_face_update(settings, state);
|
||||
break;
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
case EVENT_TICK:
|
||||
if (state->page == 0) {
|
||||
// if entering low energy mode, start tick animation
|
||||
if (event.event_type == EVENT_LOW_ENERGY_UPDATE && !watch_tick_animation_is_running()) watch_start_tick_animation(1000);
|
||||
} else {
|
||||
sunrise_sunset_alt_face_update_settings_display(event, state);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
if (state->page) {
|
||||
state->active_digit++;
|
||||
if (state->page == 1 && state->active_digit == 1) state->active_digit++; // max latitude is +- 90, no hundreds place
|
||||
if (state->active_digit > 5) {
|
||||
state->active_digit = 0;
|
||||
state->page = (state->page + 1) % 3;
|
||||
sunrise_sunset_alt_face_update_location_register(state);
|
||||
}
|
||||
sunrise_sunset_alt_face_update_settings_display(event, context);
|
||||
} else if (_location_count <= 1) {
|
||||
movement_illuminate_led();
|
||||
}
|
||||
if (state->page == 0) {
|
||||
movement_request_tick_frequency(1);
|
||||
sunrise_sunset_alt_face_update(settings, state);
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
if (_location_count <= 1) break;
|
||||
else if (!state->page) movement_illuminate_led();
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
if (state->page == 0 && _location_count > 1) {
|
||||
state->longLatToUse = (state->longLatToUse + 1) % _location_count;
|
||||
sunrise_sunset_alt_face_update(settings, state);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
if (state->page) {
|
||||
sunrise_sunset_alt_face_advance_digit(state);
|
||||
sunrise_sunset_alt_face_update_settings_display(event, context);
|
||||
} else {
|
||||
state->rise_index = (state->rise_index + 1) % 6;
|
||||
sunrise_sunset_alt_face_update(settings, state);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (state->page == 0) {
|
||||
if (state->longLatToUse != 0) {
|
||||
state->longLatToUse = 0;
|
||||
sunrise_sunset_alt_face_update(settings, state);
|
||||
break;
|
||||
}
|
||||
state->page++;
|
||||
state->active_digit = 0;
|
||||
watch_clear_display();
|
||||
movement_request_tick_frequency(4);
|
||||
sunrise_sunset_alt_face_update_settings_display(event, context);
|
||||
}
|
||||
else {
|
||||
state->active_digit = 0;
|
||||
state->page = 0;
|
||||
sunrise_sunset_alt_face_update_location_register(state);
|
||||
sunrise_sunset_alt_face_update(settings, state);
|
||||
}
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
if (watch_get_backup_data(1) == 0) {
|
||||
// if no location set, return home
|
||||
movement_move_to_face(0);
|
||||
} else if (state->page || state->rise_index) {
|
||||
// otherwise on timeout, exit settings mode and return to the next sunrise or sunset
|
||||
state->page = 0;
|
||||
state->rise_index = 0;
|
||||
movement_request_tick_frequency(1);
|
||||
sunrise_sunset_alt_face_update(settings, state);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void sunrise_sunset_alt_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
sunrise_sunset_alt_state_t *state = (sunrise_sunset_alt_state_t *)context;
|
||||
state->page = 0;
|
||||
state->active_digit = 0;
|
||||
state->rise_index = 0;
|
||||
sunrise_sunset_alt_face_update_location_register(state);
|
||||
}
|
||||
94
movement/watch_faces/complication/sunrise_sunset_alt_face.h
Normal file
94
movement/watch_faces/complication/sunrise_sunset_alt_face.h
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Joey Castillo
|
||||
*
|
||||
* 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 SUNRISE_SUNSET_ALT_FACE_H_
|
||||
#define SUNRISE_SUNSET_ALT_FACE_H_
|
||||
|
||||
/*
|
||||
* SUNRISE & SUNSET FACE
|
||||
*
|
||||
* The Sunrise/Sunset face is designed to display the next sunrise or sunset
|
||||
* for a given location. It also functions as an interface for setting the
|
||||
* location register, which other watch faces can use for various purposes.
|
||||
*
|
||||
* Refer to the wiki for usage instructions:
|
||||
* https://www.sensorwatch.net/docs/watchfaces/complication/#sunrisesunset
|
||||
*/
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t sign: 1; // 0-1
|
||||
uint8_t hundreds: 1; // 0-1, ignored for latitude
|
||||
uint8_t tens: 4; // 0-9 (must wrap at 10)
|
||||
uint8_t ones: 4; // 0-9 (must wrap at 10)
|
||||
uint8_t tenths: 4; // 0-9 (must wrap at 10)
|
||||
uint8_t hundredths: 4; // 0-9 (must wrap at 10)
|
||||
} sunrise_sunset_alt_lat_lon_settings_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t page;
|
||||
uint8_t rise_index;
|
||||
uint8_t active_digit;
|
||||
bool location_changed;
|
||||
sunrise_sunset_alt_lat_lon_settings_t working_latitude;
|
||||
sunrise_sunset_alt_lat_lon_settings_t working_longitude;
|
||||
uint8_t longLatToUse;
|
||||
} sunrise_sunset_alt_state_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t timestamp;
|
||||
watch_date_time *event;
|
||||
const char *abreviation;
|
||||
} SolarEvent;
|
||||
|
||||
void sunrise_sunset_alt_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void sunrise_sunset_alt_face_activate(movement_settings_t *settings, void *context);
|
||||
bool sunrise_sunset_alt_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void sunrise_sunset_alt_face_resign(movement_settings_t *settings, void *context);
|
||||
|
||||
#define sunrise_sunset_alt_face ((const watch_face_t){ \
|
||||
sunrise_sunset_alt_face_setup, \
|
||||
sunrise_sunset_alt_face_activate, \
|
||||
sunrise_sunset_alt_face_loop, \
|
||||
sunrise_sunset_alt_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
typedef struct {
|
||||
char name[2];
|
||||
int16_t latitude;
|
||||
int16_t longitude;
|
||||
} alt_long_lat_presets_t;
|
||||
|
||||
static const alt_long_lat_presets_t alt_longLatPresets[] =
|
||||
{
|
||||
{ .name = " "}, // Default, the long and lat get replaced by what's set in the watch
|
||||
// { .name = "dc", .latitude = 3883, .longitude = -7711 }, // Alexandria, VA
|
||||
// { .name = "Ny", .latitude = 4072, .longitude = -7401 }, // New York City, NY
|
||||
// { .name = "LA", .latitude = 3405, .longitude = -11824 }, // Los Angeles, CA
|
||||
// { .name = "dE", .latitude = 4221, .longitude = -8305 }, // Detroit, MI
|
||||
};
|
||||
|
||||
#endif // SUNRISE_SUNSET_FACE_H_
|
||||
234
movement/watch_faces/complication/wareki_face.c
Normal file
234
movement/watch_faces/complication/wareki_face.c
Normal file
@@ -0,0 +1,234 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "wareki_face.h"
|
||||
#include "filesystem.h"
|
||||
#include "watch_utility.h"
|
||||
|
||||
|
||||
//Long press status flag
|
||||
static bool _alarm_button_press;
|
||||
static bool _light_button_press;
|
||||
|
||||
|
||||
void wareki_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
|
||||
(void) watch_face_index;
|
||||
|
||||
//printf("wareki_setup() \n");
|
||||
(void) settings;
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(wareki_state_t));
|
||||
memset(*context_ptr, 0, sizeof(wareki_state_t));
|
||||
// Do any one-time tasks in here; the inside of this conditional happens only at boot.
|
||||
|
||||
//debug code
|
||||
// watch_date_time datetime = watch_rtc_get_date_time();
|
||||
// datetime.unit.year = 2022 - WATCH_RTC_REFERENCE_YEAR;
|
||||
// datetime.unit.month = 12;
|
||||
// datetime.unit.day = 31;
|
||||
// datetime.unit.hour = 23;
|
||||
// datetime.unit.minute= 59;
|
||||
// datetime.unit.second= 30;
|
||||
// watch_rtc_set_date_time(datetime);
|
||||
// settings->bit.clock_mode_24h = true; //24時間表記
|
||||
// settings->bit.to_interval = 1;//0=60sec 1=2m 2=5m 3=30m
|
||||
// watch_store_backup_data(settings->reg, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// splash view
|
||||
static void draw_wareki_splash(wareki_state_t *state) {
|
||||
(void) state;
|
||||
char buf[11];
|
||||
|
||||
watch_clear_colon();
|
||||
|
||||
sprintf(buf, "%s","wa ------");
|
||||
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
|
||||
//draw year and Japanese wareki
|
||||
static void draw_year_and_wareki(wareki_state_t *state) {
|
||||
char buf[27];
|
||||
|
||||
if(state->disp_year < REIWA_GANNEN){
|
||||
//Heisei
|
||||
sprintf(buf, " h%2d%4d ", (int)state->disp_year - HEISEI_GANNEN + 1, (int)state->disp_year);
|
||||
}
|
||||
else{
|
||||
//Reiwa
|
||||
sprintf(buf, " r%2d%4d ", (int)state->disp_year - REIWA_GANNEN + 1 , (int)state->disp_year);
|
||||
}
|
||||
watch_display_string(buf, 0);
|
||||
}
|
||||
|
||||
|
||||
void wareki_activate(movement_settings_t *settings, void *context) {
|
||||
|
||||
//printf("wareki_activate() \n");
|
||||
|
||||
(void) settings;
|
||||
wareki_state_t *state = (wareki_state_t *)context;
|
||||
|
||||
if (watch_tick_animation_is_running()) watch_stop_tick_animation();
|
||||
|
||||
state->active = false;
|
||||
|
||||
_alarm_button_press = false;
|
||||
_light_button_press = false;
|
||||
|
||||
state->real_year = watch_rtc_get_date_time().unit.year + WATCH_RTC_REFERENCE_YEAR;
|
||||
state->start_year = state->real_year;
|
||||
state->disp_year = state->real_year;
|
||||
|
||||
movement_request_tick_frequency(1);
|
||||
}
|
||||
|
||||
|
||||
void addYear(wareki_state_t* state,int count){
|
||||
|
||||
state->disp_year = state->disp_year + count;
|
||||
|
||||
if(state->disp_year > REIWA_LIMIT ){
|
||||
state->disp_year = REIWA_LIMIT;
|
||||
}
|
||||
else{
|
||||
//watch_buzzer_play_note(BUZZER_NOTE_C8, 30);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void subYear(wareki_state_t* state,int count){
|
||||
|
||||
state->disp_year = state->disp_year - count;
|
||||
|
||||
if(state->disp_year < 1989 ){
|
||||
state->disp_year = 1989;
|
||||
}
|
||||
else{
|
||||
//watch_buzzer_play_note(BUZZER_NOTE_C7, 30);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool wareki_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
wareki_state_t *state = (wareki_state_t *)context;
|
||||
|
||||
state->real_year = watch_rtc_get_date_time().unit.year + WATCH_RTC_REFERENCE_YEAR;
|
||||
|
||||
if( state->real_year != state->start_year ){
|
||||
state->start_year = state->real_year;
|
||||
state->disp_year = state->real_year;
|
||||
}
|
||||
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
draw_wareki_splash(state);
|
||||
break;
|
||||
case EVENT_MODE_BUTTON_UP:
|
||||
movement_move_to_next_face();
|
||||
break;
|
||||
|
||||
case EVENT_LOW_ENERGY_UPDATE:
|
||||
case EVENT_TICK:
|
||||
|
||||
//printf("tick %d\n",state->disp_year );
|
||||
|
||||
if (_alarm_button_press && watch_get_pin_level(BTN_ALARM)){
|
||||
//printf("ALARM ON\n");
|
||||
}
|
||||
else{
|
||||
//printf("ALARM OFF\n");
|
||||
_alarm_button_press = false;
|
||||
}
|
||||
|
||||
if (_light_button_press && watch_get_pin_level(BTN_LIGHT)){
|
||||
//printf("LIGHT ON\n");
|
||||
}
|
||||
else{
|
||||
//printf("LIGHT OFF\n");
|
||||
_light_button_press = false;
|
||||
}
|
||||
|
||||
if (_alarm_button_press) {
|
||||
addYear(state,1);
|
||||
}
|
||||
if (_light_button_press) {
|
||||
subYear(state,1);
|
||||
}
|
||||
|
||||
draw_year_and_wareki(state);
|
||||
|
||||
break;
|
||||
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
//printf("LIGHT DOWN\n");
|
||||
subYear(state,1);
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
//printf("LIGHTPRESS \n");
|
||||
_light_button_press = true;
|
||||
movement_request_tick_frequency(8);
|
||||
break;
|
||||
case EVENT_LIGHT_LONG_UP:
|
||||
//printf("LIGHTPRESS UP\n");
|
||||
_light_button_press = false;
|
||||
movement_request_tick_frequency(4);
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
//printf("LIGHT UP\n");
|
||||
_light_button_press = false;
|
||||
movement_request_tick_frequency(4);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_DOWN:
|
||||
//printf("ALARM DOWN\n");
|
||||
addYear(state,1);
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
//printf("LONGPRESS \n");
|
||||
_alarm_button_press = true;
|
||||
movement_request_tick_frequency(8);
|
||||
break;
|
||||
case EVENT_ALARM_LONG_UP:
|
||||
//printf("LONGPRESS UP\n");
|
||||
_alarm_button_press = false;
|
||||
movement_request_tick_frequency(4);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
//printf("ALARM UP\n");
|
||||
movement_request_tick_frequency(4);
|
||||
break;
|
||||
|
||||
case EVENT_TIMEOUT:
|
||||
//printf("time out ! \n");
|
||||
movement_move_to_face(0);
|
||||
|
||||
|
||||
break;
|
||||
//case EVENT_LOW_ENERGY_UPDATE:
|
||||
// If you did not resign in EVENT_TIMEOUT, you can use this event to update the display once a minute.
|
||||
// Avoid displaying fast-updating values like seconds, since the display won't update again for 60 seconds.
|
||||
// You should also consider starting the tick animation, to show the wearer that this is sleep mode:
|
||||
// watch_start_tick_animation(500);
|
||||
//break;
|
||||
default:
|
||||
// Movement's default loop handler will step in for any cases you don't handle above:
|
||||
// * EVENT_LIGHT_BUTTON_DOWN lights the LED
|
||||
// * EVENT_MODE_BUTTON_UP moves to the next watch face in the list
|
||||
// * EVENT_MODE_LONG_PRESS returns to the first watch face (or skips to the secondary watch face, if configured)
|
||||
// You can override any of these behaviors by adding a case for these events to this switch statement.
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void wareki_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
34
movement/watch_faces/complication/wareki_face.h
Normal file
34
movement/watch_faces/complication/wareki_face.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef WAREKI_FACE_H_
|
||||
#define WAREKI_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
#define REIWA_LIMIT 2018 + 31
|
||||
#define REIWA_GANNEN 2019
|
||||
#define HEISEI_GANNEN 1989
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
uint32_t disp_year; //Current displayed year
|
||||
uint32_t start_year; //Year when this screen was launched
|
||||
uint32_t real_year; //The actual current year
|
||||
} wareki_state_t;
|
||||
|
||||
|
||||
void wareki_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void wareki_activate(movement_settings_t *settings, void *context);
|
||||
bool wareki_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void wareki_resign(movement_settings_t *settings, void *context);
|
||||
void addYear(wareki_state_t* state,int count);
|
||||
void subYear(wareki_state_t* state,int count);
|
||||
|
||||
#define wareki_face ((const watch_face_t){ \
|
||||
wareki_setup, \
|
||||
wareki_activate, \
|
||||
wareki_loop, \
|
||||
wareki_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // WAREKI_FACE_H_
|
||||
|
||||
@@ -38,7 +38,7 @@ void beeps_face_setup(movement_settings_t *settings, uint8_t watch_face_index, v
|
||||
|
||||
void beeps_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
beeps_state_t *state = (beeps_state_t *)context;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
static void _beep_face_update_lcd(beeps_state_t *state) {
|
||||
|
||||
@@ -94,6 +94,7 @@ static void _lis2dw_logging_face_update_display(movement_settings_t *settings, l
|
||||
watch_display_string(buf, 0);
|
||||
if (set_leading_zero)
|
||||
watch_display_string("0", 4);
|
||||
printf("%s\n", buf);
|
||||
}
|
||||
|
||||
static void _lis2dw_logging_face_log_data(lis2dw_logger_state_t *logger_state) {
|
||||
@@ -142,7 +143,7 @@ void lis2dw_logging_face_activate(movement_settings_t *settings, void *context)
|
||||
|
||||
logger_state->display_index = 0;
|
||||
logger_state->log_ticks = 0;
|
||||
watch_enable_digital_input(A0);
|
||||
watch_enable_digital_input(A4);
|
||||
}
|
||||
|
||||
bool lis2dw_logging_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
@@ -196,7 +197,7 @@ bool lis2dw_logging_face_loop(movement_event_t event, movement_settings_t *setti
|
||||
void lis2dw_logging_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
watch_disable_digital_input(A0);
|
||||
watch_disable_digital_input(A4);
|
||||
}
|
||||
|
||||
bool lis2dw_logging_face_wants_background_task(movement_settings_t *settings, void *context) {
|
||||
|
||||
162
movement/watch_faces/sensor/accel_interrupt_count_face.c
Normal file
162
movement/watch_faces/sensor/accel_interrupt_count_face.c
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Joey Castillo
|
||||
*
|
||||
* 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 "accel_interrupt_count_face.h"
|
||||
#include "lis2dw.h"
|
||||
#include "watch.h"
|
||||
|
||||
// hacky hacky!
|
||||
uint32_t *ptr_to_count = 0;
|
||||
|
||||
void accel_interrupt_handler(void);
|
||||
void accel_interrupt_handler(void) {
|
||||
(*ptr_to_count)++;
|
||||
}
|
||||
|
||||
static void _accel_interrupt_count_face_update_display(accel_interrupt_count_state_t *state) {
|
||||
char buf[11];
|
||||
|
||||
if (state->running) {
|
||||
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
} else {
|
||||
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
|
||||
}
|
||||
|
||||
// "AC"celerometer "IN"terrupts
|
||||
snprintf(buf, 11, "AC1N%6ld", state->count);
|
||||
watch_display_string(buf, 0);
|
||||
printf("%s\n", buf);
|
||||
}
|
||||
|
||||
static void _accel_interrupt_count_face_configure_threshold(uint8_t threshold) {
|
||||
lis2dw_configure_wakeup_int1(threshold, false, true);
|
||||
}
|
||||
|
||||
void accel_interrupt_count_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
|
||||
(void) settings;
|
||||
(void) watch_face_index;
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(accel_interrupt_count_state_t));
|
||||
memset(*context_ptr, 0, sizeof(accel_interrupt_count_state_t));
|
||||
ptr_to_count = &((accel_interrupt_count_state_t *)*context_ptr)->count;
|
||||
watch_enable_i2c();
|
||||
lis2dw_begin();
|
||||
lis2dw_set_low_power_mode(LIS2DW_LP_MODE_2); // lowest power 14-bit mode, 25 Hz is 3.5 µA @ 1.8V w/ low noise, 3µA without
|
||||
lis2dw_set_low_noise_mode(true); // consumes a little more power
|
||||
lis2dw_set_range(LIS2DW_CTRL6_VAL_RANGE_4G);
|
||||
lis2dw_set_data_rate(LIS2DW_DATA_RATE_25_HZ); // is this enough?
|
||||
|
||||
// threshold is 1/64th of full scale, so for a FS of ±4G this is 1.25G
|
||||
((accel_interrupt_count_state_t *)*context_ptr)->threshold = 10;
|
||||
_accel_interrupt_count_face_configure_threshold(((accel_interrupt_count_state_t *)*context_ptr)->threshold);
|
||||
}
|
||||
}
|
||||
|
||||
void accel_interrupt_count_face_activate(movement_settings_t *settings, void *context) {
|
||||
accel_interrupt_count_state_t *state = (accel_interrupt_count_state_t *)context;
|
||||
|
||||
// never in settings mode at the start
|
||||
state->is_setting = false;
|
||||
|
||||
// force LE interval to never sleep
|
||||
settings->bit.le_interval = 0;
|
||||
}
|
||||
|
||||
bool accel_interrupt_count_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
accel_interrupt_count_state_t *state = (accel_interrupt_count_state_t *)context;
|
||||
|
||||
if (state->is_setting) {
|
||||
switch (event.event_type) {
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
state->new_threshold = (state->new_threshold + 1) % 64;
|
||||
// fall through
|
||||
case EVENT_TICK:
|
||||
{
|
||||
char buf[11];
|
||||
snprintf(buf, 11, "TH %4d ", state->new_threshold);
|
||||
watch_display_string(buf, 0);
|
||||
printf("%s\n", buf);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
lis2dw_configure_wakeup_int1(state->threshold, false, true);
|
||||
state->threshold = state->new_threshold;
|
||||
state->is_setting = false;
|
||||
break;
|
||||
default:
|
||||
movement_default_loop_handler(event, settings);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (event.event_type) {
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
movement_illuminate_led();
|
||||
|
||||
// if stopped, reset the count
|
||||
if (!state->running) {
|
||||
state->count = 0;
|
||||
}
|
||||
_accel_interrupt_count_face_update_display(state);
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
if (state->running) {
|
||||
state->running = false;
|
||||
watch_register_interrupt_callback(A4, NULL, INTERRUPT_TRIGGER_RISING);
|
||||
} else {
|
||||
state->running = true;
|
||||
watch_register_interrupt_callback(A4, accel_interrupt_handler, INTERRUPT_TRIGGER_RISING);
|
||||
}
|
||||
_accel_interrupt_count_face_update_display(state);
|
||||
break;
|
||||
case EVENT_ACTIVATE:
|
||||
case EVENT_TICK:
|
||||
_accel_interrupt_count_face_update_display(state);
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (!state->running) {
|
||||
state->new_threshold = state->threshold;
|
||||
state->is_setting = true;
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
movement_default_loop_handler(event, settings);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void accel_interrupt_count_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
bool accel_interrupt_count_face_wants_background_task(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
return false;
|
||||
}
|
||||
58
movement/watch_faces/sensor/accel_interrupt_count_face.h
Normal file
58
movement/watch_faces/sensor/accel_interrupt_count_face.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Joey Castillo
|
||||
*
|
||||
* 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
|
||||
|
||||
/*
|
||||
* Accelerometer Interrupt Counter
|
||||
*
|
||||
* This is an experimental watch face for counting the number of interrupts that
|
||||
* the Sensor Watch Motion acceleromoeter board fires. I expect it will be removed
|
||||
* once we integrate accelerometer functionality more deeply into Movement.
|
||||
*/
|
||||
|
||||
#include "movement.h"
|
||||
#include "watch.h"
|
||||
|
||||
typedef struct {
|
||||
uint32_t count;
|
||||
uint8_t new_threshold;
|
||||
uint8_t threshold;
|
||||
bool running;
|
||||
bool is_setting;
|
||||
} accel_interrupt_count_state_t;
|
||||
|
||||
void accel_interrupt_count_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void accel_interrupt_count_face_activate(movement_settings_t *settings, void *context);
|
||||
bool accel_interrupt_count_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void accel_interrupt_count_face_resign(movement_settings_t *settings, void *context);
|
||||
bool accel_interrupt_count_face_wants_background_task(movement_settings_t *settings, void *context);
|
||||
|
||||
#define accel_interrupt_count_face ((const watch_face_t){ \
|
||||
accel_interrupt_count_face_setup, \
|
||||
accel_interrupt_count_face_activate, \
|
||||
accel_interrupt_count_face_loop, \
|
||||
accel_interrupt_count_face_resign, \
|
||||
accel_interrupt_count_face_wants_background_task, \
|
||||
})
|
||||
154
movement/watch_faces/sensor/minmax_face.c
Normal file
154
movement/watch_faces/sensor/minmax_face.c
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Mark Blyth
|
||||
*
|
||||
* 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 "minmax_face.h"
|
||||
#include "thermistor_driver.h"
|
||||
#include "watch.h"
|
||||
|
||||
|
||||
static float _get_displayed_temperature_c(minmax_state_t *state){
|
||||
float min_temp = 1000;
|
||||
float max_temp = -1000;
|
||||
for(int i = 0; i < LOGGING_DATA_POINTS; i++){
|
||||
if(state->hourly_maxs[i] > max_temp){
|
||||
max_temp = state->hourly_maxs[i];
|
||||
}
|
||||
if(state->hourly_mins[i] < min_temp){
|
||||
min_temp = state->hourly_mins[i];
|
||||
}
|
||||
}
|
||||
if(state->show_min) return min_temp;
|
||||
return max_temp;
|
||||
}
|
||||
|
||||
|
||||
static void _minmax_face_log_data(minmax_state_t *logger_state) {
|
||||
thermistor_driver_enable();
|
||||
size_t pos = (size_t) watch_rtc_get_date_time().unit.hour;
|
||||
float temp_c = thermistor_driver_get_temperature();
|
||||
// If no data yet, initialise with current temperature
|
||||
if(!logger_state->have_logged){
|
||||
logger_state->have_logged = true;
|
||||
for(int i=0; i<LOGGING_DATA_POINTS; i++){
|
||||
logger_state->hourly_mins[i] = temp_c;
|
||||
logger_state->hourly_maxs[i] = temp_c;
|
||||
}
|
||||
}
|
||||
// On new hour, update lists to current temperature
|
||||
else if(watch_rtc_get_date_time().unit.minute < 2){
|
||||
logger_state->hourly_mins[pos] = temp_c;
|
||||
logger_state->hourly_maxs[pos] = temp_c;
|
||||
}
|
||||
// Log hourly highs and lows
|
||||
else if(logger_state->hourly_mins[pos] > temp_c){
|
||||
logger_state->hourly_mins[pos] = temp_c;
|
||||
}
|
||||
else if(logger_state->hourly_maxs[pos] < temp_c){
|
||||
logger_state->hourly_maxs[pos] = temp_c;
|
||||
}
|
||||
thermistor_driver_disable();
|
||||
}
|
||||
|
||||
static void _minmax_face_update_display(float temperature_c, bool in_fahrenheit) {
|
||||
char buf[14];
|
||||
if (in_fahrenheit) {
|
||||
sprintf(buf, "%4.0f#F", temperature_c * 1.8 + 32.0);
|
||||
} else {
|
||||
sprintf(buf, "%4.0f#C", temperature_c);
|
||||
}
|
||||
watch_display_string(buf, 4);
|
||||
}
|
||||
|
||||
|
||||
void minmax_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
|
||||
(void) settings;
|
||||
(void) watch_face_index;
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(minmax_state_t));
|
||||
memset(*context_ptr, 0, sizeof(minmax_state_t));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void minmax_face_activate(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
minmax_state_t *state = (minmax_state_t *)context;
|
||||
state->show_min = true;
|
||||
watch_display_string("MN", 0); // Start with minimum temp
|
||||
}
|
||||
|
||||
bool minmax_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
|
||||
minmax_state_t *state = (minmax_state_t *)context;
|
||||
float temp_c;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
temp_c = _get_displayed_temperature_c(state);
|
||||
_minmax_face_update_display(temp_c, settings->bit.use_imperial_units);
|
||||
break;
|
||||
|
||||
case EVENT_LIGHT_LONG_PRESS:
|
||||
settings->bit.use_imperial_units = !settings->bit.use_imperial_units;
|
||||
temp_c = _get_displayed_temperature_c(state);
|
||||
_minmax_face_update_display(temp_c, settings->bit.use_imperial_units);
|
||||
break;
|
||||
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
state->show_min = !state->show_min;
|
||||
if(state->show_min){
|
||||
watch_display_string("MN", 0);
|
||||
} else {
|
||||
watch_display_string("MX", 0);
|
||||
}
|
||||
temp_c = _get_displayed_temperature_c(state);
|
||||
_minmax_face_update_display(temp_c, settings->bit.use_imperial_units);
|
||||
break;
|
||||
|
||||
case EVENT_TIMEOUT:
|
||||
movement_move_to_face(0);
|
||||
break;
|
||||
case EVENT_BACKGROUND_TASK:
|
||||
_minmax_face_log_data(state);
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event, settings);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void minmax_face_resign(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
}
|
||||
|
||||
|
||||
bool minmax_face_wants_background_task(movement_settings_t *settings, void *context) {
|
||||
(void) settings;
|
||||
(void) context;
|
||||
// this will get called at the top of each minute; always request bg task
|
||||
return true;
|
||||
}
|
||||
69
movement/watch_faces/sensor/minmax_face.h
Normal file
69
movement/watch_faces/sensor/minmax_face.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Mark Blyth
|
||||
*
|
||||
* 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 MINMAX_FACE_H_
|
||||
#define MINMAX_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
#include "watch.h"
|
||||
|
||||
#define LOGGING_DATA_POINTS (24)
|
||||
|
||||
/*
|
||||
* Log for the min. and max. temperature over the last 24h.
|
||||
*
|
||||
* Temperature is logged once a minute, every minute. Results are
|
||||
* stored, noting the highest and lowest temperatures observed within
|
||||
* any given hour. The watch face then displays the minimum or maximum
|
||||
* temperature recorded over the last 24h.
|
||||
*
|
||||
* A long press of the light button changes units between Celsius and
|
||||
* Fahrenheit. Pressing the alarm button switches between displaying the
|
||||
* minimum and maximum observed temperatures. If no buttons are pressed,
|
||||
* the watch face will eventually time out and return home.
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
bool show_min;
|
||||
bool have_logged;
|
||||
float hourly_mins[LOGGING_DATA_POINTS];
|
||||
float hourly_maxs[LOGGING_DATA_POINTS];
|
||||
} minmax_state_t;
|
||||
|
||||
void minmax_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
|
||||
void minmax_face_activate(movement_settings_t *settings, void *context);
|
||||
bool minmax_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
|
||||
void minmax_face_resign(movement_settings_t *settings, void *context);
|
||||
bool minmax_face_wants_background_task(movement_settings_t *settings, void *context);
|
||||
|
||||
#define minmax_face ((const watch_face_t){ \
|
||||
minmax_face_setup, \
|
||||
minmax_face_activate, \
|
||||
minmax_face_loop, \
|
||||
minmax_face_resign, \
|
||||
minmax_face_wants_background_task, \
|
||||
})
|
||||
|
||||
#endif // MINMAX_FACE_H_
|
||||
|
||||
Reference in New Issue
Block a user