'movement' -> 'legacy' to signal things we still need to bring in

This commit is contained in:
Joey Castillo
2024-11-27 10:56:50 -05:00
parent bc08c5a05e
commit 9719567047
221 changed files with 6 additions and 6 deletions

View File

@@ -0,0 +1,722 @@
/*
* MIT License
*
* Copyright (c) 2023 Gabor L Ugray
*
* 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.
*/
/* ** TODO
* ===================
* -- Additional power-saving optimizations
*/
#include <stdlib.h>
#include <string.h>
#include "activity_face.h"
#include "chirpy_tx.h"
#include "watch.h"
#include "watch_utility.h"
// ===========================================================================
// This part is configurable: you can edit values here to customize you activity face
// In particular, with num_enabled_activities and enabled_activities you can choose a subset of the
// activities that you want to see in your watch.
// You can also add new items to activity_names, but don't redefine or remove existing ones.
// If a logged activity is shorter than this, then it won't be added to log when it ends.
// This way scarce log slots are not taken up by aborted events that weren't real activities.
static const uint16_t activity_min_length_sec = 60;
// Supported activities. ID of activity is index in this buffer
// W e should never change order or redefine items, only add new items when needed.
static const char activity_names[][7] = {
" bIKE ",
"uuaLK ",
" rUn ",
"DAnCE ",
" yOgA ",
"CrOSS ",
"Suuinn",
"ELLIP ",
" gYnn",
" rOuu",
"SOCCEr",
" FOOTb",
" bALL ",
" SKI ",
};
// Currently enabled activities. This makes picking on first subface easier: why show activities you personally never do.
static const uint8_t enabled_activities[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
// Number of currently enabled activities (size of enabled_activities).
static const uint8_t num_enabled_activities = sizeof(enabled_activities) / sizeof(uint8_t);
// End configurable section
// ===========================================================================
// One logged activity
typedef struct __attribute__((__packed__)) {
// Activity's start time
watch_date_time_t start_time;
// Total duration of activity, including time spend in paus
uint16_t total_sec;
// Number of seconds the activity was paused
uint16_t pause_sec;
// Type of activity (index in activity_names)
uint8_t activity_type;
} activity_item_t;
#define MAX_ACTIVITY_SECONDS 28800 // 8 hours = 28800 sec
// Size of (fixed) buffer to log activites. Takes up x9 bytes in SRAM if face is installed.
#define ACTIVITY_LOG_SZ 99
// Number of activities in buffer.
static uint8_t activity_log_count = 0;
// Buffer with all logged activities.
static activity_item_t activity_log_buffer[ACTIVITY_LOG_SZ];
#define CHIRPY_PREFIX_LEN 2
// First two bytes chirped out, to identify transmission as from the activity face
static const uint8_t activity_chirpy_prefix[CHIRPY_PREFIX_LEN] = {0x27, 0x00};
// The face's different UI modes (views).
typedef enum {
ACTM_CHOOSE = 0,
ACTM_LOGGING,
ACTM_PAUSED,
ACTM_DONE,
ACTM_LOGSIZE,
ACTM_CHIRP,
ACTM_CHIRPING,
ACTM_CLEAR,
ACTM_CLEAR_CONFIRM,
ACTM_CLEAR_DONE,
} activity_mode_t;
// The full state of the activity face
typedef struct {
// Current mode (which secondary face, or ongoing operation like logging)
activity_mode_t mode;
// Index of currently selected activity in enabled_activities
uint8_t type_ix;
// Used for different things depending on mode
// In ACTM_DONE: countdown for animation, before returning to start face
// In ACTM_LOGGING and ACTM_PAUSED: drives blinking colon and alternating time display
// In ACTM_LOGSIZE, ACTM_CLEAR: enables timeout return to choose screen
uint16_t counter;
// Start of currently logged activity, if any
watch_date_time_t start_time;
// Total seconds elapsed since logging started
uint16_t curr_total_sec;
// Total paused seconds in current log
uint16_t curr_pause_sec;
// Helps us handle 1/64 ticks during transmission; including countdown timer
chirpy_tick_state_t chirpy_tick_state;
// Used by chirpy encoder during transmission
chirpy_encoder_state_t chirpy_encoder_state;
// 0: Running normally
// 1: In LE mode
// 2: Just woke up from LE mode. Will go to 0 after ignoring ALARM_BUTTON_UP.
uint8_t le_state;
} activity_state_t;
#define ACTIVITY_BUF_SZ 14
// Temp buffer used for sprintf'ing content for the display.
char activity_buf[ACTIVITY_BUF_SZ];
// Needed by _activity_get_next_byte to keep track of where we are in transmission
uint16_t *activity_seq_pos;
static void _activity_clear_buffers() {
// Clear activity buffer; 0xcd is good for diagnostics
memset(activity_log_buffer, 0xcd, ACTIVITY_LOG_SZ * sizeof(activity_item_t));
// Clear display buffer
memset(activity_buf, 0, ACTIVITY_BUF_SZ);
}
static void _activity_display_choice(activity_state_t *state);
static void _activity_update_logging_screen(activity_state_t *state);
static uint8_t _activity_get_next_byte(uint8_t *next_byte);
void activity_face_setup(uint8_t watch_face_index, void **context_ptr) {
(void)watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(activity_state_t));
memset(*context_ptr, 0, sizeof(activity_state_t));
// This happens only at boot
_activity_clear_buffers();
}
// Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.
}
void activity_face_activate(void *context) {
(void)context;
// Not using this function. Calling _activity_activate from the event handler.
// That is what we get both when the face is shown upon navigation by MODE,
// and when waking from low energy state.
}
// Called from the ACTIVATE event handler in the loop
static void _activity_activate(activity_state_t *state) {
// If waking from low-energy state and currently logging: update seconds values
// Those are not up-to-date because ticks have not been coming
if (state->le_state != 0 && state->mode == ACTM_LOGGING) {
state->le_state = 2;
watch_date_time_t now = watch_rtc_get_date_time();
uint32_t now_timestamp = watch_utility_date_time_to_unix_time(now, 0);
uint32_t start_timestamp = watch_utility_date_time_to_unix_time(state->start_time, 0);
uint32_t total_seconds = now_timestamp - start_timestamp;
state->curr_total_sec = total_seconds;
_activity_update_logging_screen(state);
}
// Regular activation: start from defaults
else {
state->le_state = 0;
state->mode = 0;
state->type_ix = 0;
_activity_display_choice(state);
}
}
static void _activity_display_choice(activity_state_t *state) {
watch_display_string("AC", 0);
// If buffer is full: We say "FULL"
if (activity_log_count >= ACTIVITY_LOG_SZ) {
watch_display_string(" FULL ", 4);
}
// Otherwise, we show currently activity
else {
uint8_t activity_ix = enabled_activities[state->type_ix];
const char *name = activity_names[activity_ix];
watch_display_string((char *)name, 4);
}
}
const uint8_t activity_anim_pixels[][2] = {
{1, 4}, // TL
{0, 5}, // BL
{0, 6}, // BOT
{1, 6}, // BR
{2, 5}, // TR
{2, 4}, // TOP
// {2, 4}, // MID
};
static void _activity_update_logging_screen(activity_state_t *state) {
watch_duration_t duration;
watch_display_string("AC ", 0);
// If we're in LE state: per-minute update is special
if (state->le_state == 1) {
watch_date_time_t now = watch_rtc_get_date_time();
uint32_t now_timestamp = watch_utility_date_time_to_unix_time(now, 0);
uint32_t start_timestamp = watch_utility_date_time_to_unix_time(state->start_time, 0);
uint32_t total_seconds = now_timestamp - start_timestamp;
duration = watch_utility_seconds_to_duration(total_seconds);
sprintf(activity_buf, " %d%02d ", duration.hours, duration.minutes);
watch_display_string(activity_buf, 4);
watch_set_colon();
watch_set_indicator(WATCH_INDICATOR_LAP);
watch_clear_indicator(WATCH_INDICATOR_PM);
watch_clear_indicator(WATCH_INDICATOR_24H);
return;
}
// Show elapsed time, or PAUSE
if ((state->counter % 5) < 3) {
watch_set_indicator(WATCH_INDICATOR_LAP);
watch_clear_indicator(WATCH_INDICATOR_PM);
watch_clear_indicator(WATCH_INDICATOR_24H);
if (state->mode == ACTM_PAUSED) {
watch_display_string(" PAUSE", 4);
watch_clear_colon();
} else {
duration = watch_utility_seconds_to_duration(state->curr_total_sec);
// Under 10 minutes: M:SS
if (state->curr_total_sec < 600) {
sprintf(activity_buf, " %01d%02d", duration.minutes, duration.seconds);
watch_display_string(activity_buf, 4);
watch_clear_colon();
}
// Under an hour: MM:SS
else if (state->curr_total_sec < 3600) {
sprintf(activity_buf, " %02d%02d", duration.minutes, duration.seconds);
watch_display_string(activity_buf, 4);
watch_clear_colon();
}
// Over an hour: H:MM:SS
// (We never go to two-digit hours; stop at 8)
else {
sprintf(activity_buf, " %d%02d%02d", duration.hours, duration.minutes, duration.seconds);
watch_display_string(activity_buf, 4);
watch_set_colon();
}
}
}
// Briefly, show time without seconds
else {
watch_clear_indicator(WATCH_INDICATOR_LAP);
watch_date_time_t now = watch_rtc_get_date_time();
uint8_t hour = now.unit.hour;
if (!movement_clock_mode_24h()) {
watch_clear_indicator(WATCH_INDICATOR_24H);
if (hour < 12)
watch_clear_indicator(WATCH_INDICATOR_PM);
else
watch_set_indicator(WATCH_INDICATOR_PM);
hour %= 12;
if (hour == 0) hour = 12;
}
else {
watch_set_indicator(WATCH_INDICATOR_24H);
watch_clear_indicator(WATCH_INDICATOR_PM);
}
sprintf(activity_buf, "%2d%02d ", hour, now.unit.minute);
watch_set_colon();
watch_display_string(activity_buf, 4);
}
}
static void _activity_quit_chirping() {
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_set_buzzer_off();
movement_request_tick_frequency(1);
}
static void _activity_chirp_tick_transmit(void *context) {
activity_state_t *state = (activity_state_t *)context;
uint8_t tone = chirpy_get_next_tone(&state->chirpy_encoder_state);
// Transmission over?
if (tone == 255) {
_activity_quit_chirping();
state->mode = ACTM_CHIRP;
state->counter = 0;
watch_display_string("AC CHIRP ", 0);
return;
}
uint16_t period = chirpy_get_tone_period(tone);
watch_set_buzzer_period_and_duty_cycle(period, 25);
watch_set_buzzer_on();
}
static void _activity_chirp_tick_countdown(void *context) {
activity_state_t *state = (activity_state_t *)context;
// Countdown over: start actual broadcast
if (state->chirpy_tick_state.seq_pos == 8 * 3) {
state->chirpy_tick_state.tick_compare = 3;
state->chirpy_tick_state.tick_count = 2; // tick_compare - 1, so it starts immediately
state->chirpy_tick_state.seq_pos = 0;
state->chirpy_tick_state.tick_fun = _activity_chirp_tick_transmit;
return;
}
// Sound or turn off buzzer
if ((state->chirpy_tick_state.seq_pos % 8) == 0) {
watch_set_buzzer_period_and_duty_cycle(NotePeriods[BUZZER_NOTE_A5], 25);
watch_set_buzzer_on();
if (state->chirpy_tick_state.seq_pos == 0) {
watch_display_string(" --- ", 4);
} else if (state->chirpy_tick_state.seq_pos == 8) {
watch_display_string(" --", 5);
} else if (state->chirpy_tick_state.seq_pos == 16) {
watch_display_string(" -", 5);
}
} else if ((state->chirpy_tick_state.seq_pos % 8) == 1) {
watch_set_buzzer_off();
}
++state->chirpy_tick_state.seq_pos;
}
static uint8_t _activity_get_next_byte(uint8_t *next_byte) {
uint16_t num_bytes = 2 + activity_log_count * sizeof(activity_item_t);
uint16_t pos = *activity_seq_pos;
// Init counter
if (pos == 0) {
sprintf(activity_buf, "%3d", activity_log_count);
watch_display_string(activity_buf, 5);
}
if (pos == num_bytes) {
return 0;
}
// Two-byte prefix
if (pos < 2) {
(*next_byte) = activity_chirpy_prefix[pos];
}
// Data
else {
pos -= 2;
uint16_t ix = pos / sizeof(activity_item_t);
const activity_item_t *itm = &activity_log_buffer[ix];
uint16_t ofs = pos % sizeof(activity_item_t);
// Update counter when starting new item
if (ofs == 0) {
sprintf(activity_buf, "%3d", activity_log_count - ix);
watch_display_string(activity_buf, 5);
}
// Do this the hard way, byte by byte, to avoid high/low endedness issues
// Higher order bytes first, is our serialization format
uint8_t val;
// watch_date_time_t start_time;
// uint16_t total_sec;
// uint16_t pause_sec;
// uint8_t activity_type;
if (ofs == 0)
val = (itm->start_time.reg & 0xff000000) >> 24;
else if (ofs == 1)
val = (itm->start_time.reg & 0x00ff0000) >> 16;
else if (ofs == 2)
val = (itm->start_time.reg & 0x0000ff00) >> 8;
else if (ofs == 3)
val = (itm->start_time.reg & 0x000000ff);
else if (ofs == 4)
val = (itm->total_sec & 0xff00) >> 8;
else if (ofs == 5)
val = (itm->total_sec & 0x00ff);
else if (ofs == 6)
val = (itm->pause_sec & 0xff00) >> 8;
else if (ofs == 7)
val = (itm->pause_sec & 0x00ff);
else
val = itm->activity_type;
(*next_byte) = val;
}
++(*activity_seq_pos);
return 1;
}
static void _activity_finish_logging(activity_state_t *state) {
// Save this activity
// If shorter than minimum for log: don't save
// Sanity check about buffer length. This should never happen, but also we never want to overrun by error
if (state->curr_total_sec >= activity_min_length_sec && activity_log_count + 1 < ACTIVITY_LOG_SZ) {
activity_item_t *itm = &activity_log_buffer[activity_log_count];
itm->start_time = state->start_time;
itm->total_sec = state->curr_total_sec;
itm->pause_sec = state->curr_pause_sec;
itm->activity_type = state->type_ix;
++activity_log_count;
}
// Go to DONE animation
// TODO: Not in LE mode
state->mode = ACTM_DONE;
watch_clear_indicator(WATCH_INDICATOR_LAP);
movement_request_tick_frequency(2);
state->counter = 6 * 1;
watch_clear_display();
watch_display_string("AC dONE ", 0);
}
static void _activity_handle_tick(activity_state_t *state) {
// Display stopwatch-like duration while logging, alternating with time
if (state->mode == ACTM_LOGGING || state->mode == ACTM_PAUSED) {
++state->counter;
++state->curr_total_sec;
if (state->mode == ACTM_PAUSED)
++state->curr_pause_sec;
// If we've reached max activity length: finish logging
if (state->curr_total_sec == MAX_ACTIVITY_SECONDS) {
_activity_finish_logging(state);
}
// Still logging: refresh display
else {
_activity_update_logging_screen(state);
}
}
// Display countown animation, and exit face when down
else if (state->mode == ACTM_DONE) {
if (state->counter == 0) {
movement_move_to_face(0);
movement_request_tick_frequency(1);
}
else {
uint8_t cd = state->counter % 6;
watch_clear_pixel(activity_anim_pixels[cd][0], activity_anim_pixels[cd][1]);
--state->counter;
cd = state->counter % 6;
watch_set_pixel(activity_anim_pixels[cd][0], activity_anim_pixels[cd][1]);
}
}
// Log size, chirp, clear: return to choose after some time
else if (state->mode == ACTM_LOGSIZE || state->mode == ACTM_CHIRP || state->mode == ACTM_CLEAR) {
++state->counter;
// Leave Log Size after 20 seconds
// Leave Clear after only 10: this is danger zone, we don't like hanging around here
// Leave Chirp after 2 minutes: most likely need to the time to fiddle with mic & Chirpy RX on the computer
uint16_t timeout = 20;
if (state->mode == ACTM_CLEAR) timeout = 10;
else if (state->mode == ACTM_CHIRP) timeout = 120;
if (state->counter > timeout) {
state->mode = ACTM_CHOOSE;
_activity_display_choice(state);
}
}
// Chirping
else if (state->mode == ACTM_CHIRPING) {
++state->chirpy_tick_state.tick_count;
if (state->chirpy_tick_state.tick_count == state->chirpy_tick_state.tick_compare) {
state->chirpy_tick_state.tick_count = 0;
state->chirpy_tick_state.tick_fun(state);
}
}
// Clear confirm: blink CLEAR
else if (state->mode == ACTM_CLEAR_CONFIRM) {
++state->counter;
if ((state->counter % 2) == 0)
watch_display_string("CLEAR ", 4);
else
watch_display_string(" ", 4);
if (state->counter > 12) {
state->mode = ACTM_CHOOSE;
_activity_display_choice(state);
movement_request_tick_frequency(1);
}
}
// Clear done: fill up zeroes, then return to choose screen
else if (state->mode == ACTM_CLEAR_DONE) {
++state->counter;
// Animation done
if (state->counter == 7) {
state->mode = ACTM_CHOOSE;
_activity_display_choice(state);
movement_request_tick_frequency(1);
return;
}
// Display current state of animation
sprintf(activity_buf, " ");
uint8_t nZeros = state->counter + 1;
if (nZeros > 6) nZeros = 6;
for (uint8_t i = 0; i < nZeros; ++i) {
activity_buf[i] = '0';
}
watch_display_string(activity_buf, 4);
}
}
static void _activity_alarm_long(activity_state_t *state) {
// On choose face: start logging activity
if (state->mode == ACTM_CHOOSE) {
// If buffer is full: Ignore this long press
if (activity_log_count >= ACTIVITY_LOG_SZ)
return;
// OK, we go ahead and start logging
state->start_time = watch_rtc_get_date_time();
state->curr_total_sec = 0;
state->curr_pause_sec = 0;
state->counter = -1;
state->mode = ACTM_LOGGING;
watch_set_indicator(WATCH_INDICATOR_LAP);
_activity_update_logging_screen(state);
}
// If logging or paused: end logging
else if (state->mode == ACTM_LOGGING || state->mode == ACTM_PAUSED) {
_activity_finish_logging(state);
}
// If chirp: kick off chirping
else if (state->mode == ACTM_CHIRP) {
// Set up our tick handling for countdown beeps
activity_seq_pos = &state->chirpy_tick_state.seq_pos;
state->chirpy_tick_state.tick_compare = 8;
state->chirpy_tick_state.tick_count = 7; // tick_compare - 1, so it starts immediately
state->chirpy_tick_state.seq_pos = 0;
state->chirpy_tick_state.tick_fun = _activity_chirp_tick_countdown;
// Set up chirpy encoder
chirpy_init_encoder(&state->chirpy_encoder_state, _activity_get_next_byte);
// Show bell; switch to 64/sec ticks
watch_set_indicator(WATCH_INDICATOR_BELL);
movement_request_tick_frequency(64);
state->mode = ACTM_CHIRPING;
}
// If clear: confirm (unless empty)
else if (state->mode == ACTM_CLEAR) {
if (activity_log_count == 0)
return;
state->mode = ACTM_CLEAR_CONFIRM;
state->counter = -1;
movement_request_tick_frequency(4);
}
// If clear confirm: do clear.
else if (state->mode == ACTM_CLEAR_CONFIRM) {
_activity_clear_buffers();
activity_log_count = 0;
state->mode = ACTM_CLEAR_DONE;
state->counter = -1;
watch_display_string("0 ", 4);
movement_request_tick_frequency(2);
}
}
static void _activity_alarm_short(activity_state_t *state) {
// In the choose face, short ALARM cycles through activities
if (state->mode == ACTM_CHOOSE) {
state->type_ix = (state->type_ix + 1) % num_enabled_activities;
_activity_display_choice(state);
}
// If logging: pause
else if (state->mode == ACTM_LOGGING) {
state->mode = ACTM_PAUSED;
state->counter = 0;
_activity_update_logging_screen(state);
}
// If paused: Update paused seconds count and return to logging
else if (state->mode == ACTM_PAUSED) {
state->mode = ACTM_LOGGING;
state->counter = 0;
_activity_update_logging_screen(state);
}
// If chirping: stoppit
else if (state->mode == ACTM_CHIRPING) {
_activity_quit_chirping();
state->mode = ACTM_CHIRP;
state->counter = 0;
watch_display_string("AC CHIRP ", 0);
}
}
static void _activity_light_short(activity_state_t *state) {
// If choose face: move to log size
if (state->mode == ACTM_CHOOSE) {
state->mode = ACTM_LOGSIZE;
state->counter = 0;
sprintf(activity_buf, "AC L#g%3d", activity_log_count);
watch_display_string(activity_buf, 0);
}
// If log size face: move to chirp
else if (state->mode == ACTM_LOGSIZE) {
state->mode = ACTM_CHIRP;
state->counter = 0;
watch_display_string("AC CHIRP ", 0);
}
// If chirp face: move to clear
else if (state->mode == ACTM_CHIRP) {
state->mode = ACTM_CLEAR;
state->counter = 0;
watch_display_string("AC CLEAR ", 0);
}
// If clear face: return to choose face
else if (state->mode == ACTM_CLEAR || state->mode == ACTM_CLEAR_CONFIRM) {
state->mode = ACTM_CHOOSE;
_activity_display_choice(state);
movement_request_tick_frequency(1);
}
// While logging or paused, light is light
else if (state->mode == ACTM_LOGGING || state->mode == ACTM_PAUSED) {
movement_illuminate_led();
}
// Otherwise, we don't do light.
}
bool activity_face_loop(movement_event_t event, void *context) {
activity_state_t *state = (activity_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
_activity_activate(state);
break;
case EVENT_TICK:
_activity_handle_tick(state);
break;
case EVENT_MODE_BUTTON_UP:
if (state->mode != ACTM_LOGGING && state->mode != ACTM_PAUSED && state->mode != ACTM_CHIRPING) {
movement_request_tick_frequency(1);
movement_move_to_next_face();
}
break;
case EVENT_LIGHT_BUTTON_UP:
_activity_light_short(state);
break;
case EVENT_ALARM_BUTTON_UP:
// We also receive ALARM press that woke us up from LE state
// Don't want to act on that as if it were a real button press for us
if (state->le_state != 2)
_activity_alarm_short(state);
else
state->le_state = 0;
break;
case EVENT_ALARM_LONG_PRESS:
_activity_alarm_long(state);
break;
case EVENT_TIMEOUT:
if (state->mode != ACTM_LOGGING && state->mode != ACTM_PAUSED &&
state->mode != ACTM_CHIRP && state->mode != ACTM_CHIRPING) {
movement_request_tick_frequency(1);
movement_move_to_face(0);
}
break;
case EVENT_LOW_ENERGY_UPDATE:
state->le_state = 1;
// If we're in paused logging mode: let's lose this activity. Pause is not meant for over an hour.
if (state->mode == ACTM_PAUSED) {
// When waking, face will revert to default screen
state->mode = ACTM_CHOOSE;
watch_display_string("AC SLEEP ", 0);
watch_clear_colon();
watch_clear_indicator(WATCH_INDICATOR_LAP);
watch_clear_indicator(WATCH_INDICATOR_PM);
}
else {
_activity_update_logging_screen(state);
watch_start_sleep_animation(500);
}
break;
default:
movement_default_loop_handler(event);
break;
}
// Return true if the watch can enter standby mode. False needed when chirping.
if (state->mode == ACTM_CHIRPING)
return false;
else
return true;
}
void activity_face_resign(void *context) {
(void)context;
// Face should only ever temporarily request a higher frequency, so by the time we're resigning,
// this should not be needed. But we don't want an error to create a situation that drains the battery.
// Rather do this defensively here.
movement_request_tick_frequency(1);
}

View File

@@ -0,0 +1,88 @@
/*
* MIT License
*
* Copyright (c) 2023 Gabor L Ugray
*
* 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 ACTIVITY_FACE_H_
#define ACTIVITY_FACE_H_
/*
* ACTIVITY watch face
*
* The Activity face lets you record activities like you would do with a fitness watch.
* It supports different activities like running, biking, rowing etc., and for each recorded activity
* it stores when it started and how long it was.
*
* You can save up to 99 activities this way. Every once in a while you can chirp them out
* using the watch's piezo buzzer as a modem, then clear the log in the watch.
* To record and decode a chirpy transmission on your computer, you can use the web app here:
* https://jealousmarkup.xyz/off/chirpy/rx/
*
* Using the face
*
* When you activate the face, it starts with the first screen to select the activity you want to log.
* ALARM cycles through the list of activities.
* LONG ALARM starts logging.
* While logging is in progress, the face alternates between the elapsed time and the current time.
* You can press ALARM to pause (e.g., while you hop in to the baker's for a croissant during your jog).
* Pressing ALARM again resumes the activity.
* LONG ALARM stops logging and saves the activity.
*
* When you're not loggin, you can press LIGHT to access the secondary faces.
* LIGHT #1 => Shows the size of the log (how many activities have been recorded).
* LIGHT #2 => The screen to chirp out the data. Press LONG ALARM to start chirping.
* LIGHT #3 => The screen to clear the log in the watch. Press LONG ALARM twice to clear data.
*
* Quirky details
*
* The face will discard short activities (less than a minute) when you press LONG ALARM to finish logging.
* These were probably logged by mistake, and it's better to save slots and chirping battery power for
* stuff that really matters.
*
* The face will continue to record an activity past the normal one-hour mark, when the watch
* enters low energy mode. However, it will always stop at 8 hours. If an activity takes that long,
* you probably just forgot to stop logging.
*
* The log is stored in regular memory. It will be lost when you remove the battery, so make
* sure you chirp it out before taking the watch apart.
*
* See the top of activity_face.c for some customization options. What you most likely want to do
* is reduce the list of activities shown on the first screen to the ones you are regularly doing.
*/
#include "movement.h"
void activity_face_setup(uint8_t watch_face_index, void ** context_ptr);
void activity_face_activate(void *context);
bool activity_face_loop(movement_event_t event, void *context);
void activity_face_resign(void *context);
#define activity_face ((const watch_face_t){ \
activity_face_setup, \
activity_face_activate, \
activity_face_loop, \
activity_face_resign, \
NULL, \
})
#endif // ACTIVITY_FACE_H_

View File

@@ -0,0 +1,270 @@
/*
* 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 <stdio.h>
#include <string.h>
#include <math.h>
#include "astronomy_face.h"
#include "watch_utility.h"
#if __EMSCRIPTEN__
#include <emscripten.h>
#endif
#define NUM_AVAILABLE_BODIES 9
static const char astronomy_available_celestial_bodies[NUM_AVAILABLE_BODIES] = {
ASTRO_BODY_SUN,
ASTRO_BODY_MERCURY,
ASTRO_BODY_VENUS,
ASTRO_BODY_MOON,
ASTRO_BODY_MARS,
ASTRO_BODY_JUPITER,
ASTRO_BODY_SATURN,
ASTRO_BODY_URANUS,
ASTRO_BODY_NEPTUNE
};
static const char astronomy_celestial_body_names[NUM_AVAILABLE_BODIES][3] = {
"SO", // Sol
"ME", // Mercury
"VE", // Venus
"LU", // Moon (Luna)
"MA", // Mars
"JU", // Jupiter
"SA", // Saturn
"UR", // Uranus
"NE" // Neptune
};
static void _astronomy_face_recalculate(astronomy_state_t *state) {
#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);
double lat = (double)browser_lat / 100.0;
double lon = (double)browser_lon / 100.0;
state->latitude_radians = astro_degrees_to_radians(lat);
state->longitude_radians = astro_degrees_to_radians(lon);
}
#endif
watch_date_time_t date_time = watch_rtc_get_date_time();
uint32_t timestamp = watch_utility_date_time_to_unix_time(date_time, movement_get_current_timezone_offset());
date_time = watch_utility_date_time_from_unix_time(timestamp, 0);
double jd = astro_convert_date_to_julian_date(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR, date_time.unit.month, date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
astro_equatorial_coordinates_t radec_precession = astro_get_ra_dec(jd, astronomy_available_celestial_bodies[state->active_body_index], state->latitude_radians, state->longitude_radians, true);
printf("\nParams to convert: %f %f %f %f %f\n",
jd,
astro_radians_to_degrees(state->latitude_radians),
astro_radians_to_degrees(state->longitude_radians),
astro_radians_to_degrees(radec_precession.right_ascension),
astro_radians_to_degrees(radec_precession.declination));
astro_horizontal_coordinates_t horiz = astro_ra_dec_to_alt_az(jd, state->latitude_radians, state->longitude_radians, radec_precession.right_ascension, radec_precession.declination);
astro_equatorial_coordinates_t radec = astro_get_ra_dec(jd, astronomy_available_celestial_bodies[state->active_body_index], state->latitude_radians, state->longitude_radians, false);
state->altitude = astro_radians_to_degrees(horiz.altitude);
state->azimuth = astro_radians_to_degrees(horiz.azimuth);
state->right_ascension = astro_radians_to_hms(radec.right_ascension);
state->declination = astro_radians_to_dms(radec.declination);
state->distance = radec.distance;
printf("Calculated coordinates for %s on %f: \n\tRA = %f / %2dh %2dm %2ds\n\tDec = %f / %3d° %3d' %3d\"\n\tAzi = %f\n\tAlt = %f\n\tDst = %f AU\n",
astronomy_celestial_body_names[state->active_body_index],
jd,
astro_radians_to_degrees(radec.right_ascension),
state->right_ascension.hours,
state->right_ascension.minutes,
state->right_ascension.seconds,
astro_radians_to_degrees(radec.declination),
state->declination.degrees,
state->declination.minutes,
state->declination.seconds,
state->altitude,
state->azimuth,
state->distance);
}
static void _astronomy_face_update(movement_event_t event, astronomy_state_t *state) {
char buf[16];
switch (state->mode) {
case ASTRONOMY_MODE_SELECTING_BODY:
watch_clear_colon();
watch_display_string(" Astro", 4);
if (event.subsecond % 2) {
watch_display_string((char *)astronomy_celestial_body_names[state->active_body_index], 0);
} else {
watch_display_string(" ", 0);
}
if (event.subsecond == 0) {
watch_display_string(" ", 2);
switch (state->animation_state) {
case 0:
watch_set_pixel(0, 7);
watch_set_pixel(2, 6);
break;
case 1:
watch_set_pixel(1, 7);
watch_set_pixel(2, 9);
break;
case 2:
watch_set_pixel(2, 7);
watch_set_pixel(0, 9);
break;
}
state->animation_state = (state->animation_state + 1) % 3;
}
break;
case ASTRONOMY_MODE_CALCULATING:
watch_clear_display();
// this takes a moment and locks the UI, flash C for "Calculating"
watch_start_character_blink('C', 100);
_astronomy_face_recalculate(state);
watch_stop_blink();
state->mode = ASTRONOMY_MODE_DISPLAYING_ALT;
// fall through
case ASTRONOMY_MODE_DISPLAYING_ALT:
sprintf(buf, "%saL%6d", astronomy_celestial_body_names[state->active_body_index], (int16_t)round(state->altitude * 100));
watch_display_string(buf, 0);
break;
case ASTRONOMY_MODE_DISPLAYING_AZI:
sprintf(buf, "%saZ%6d", astronomy_celestial_body_names[state->active_body_index], (int16_t)round(state->azimuth * 100));
watch_display_string(buf, 0);
break;
case ASTRONOMY_MODE_DISPLAYING_RA:
watch_set_colon();
sprintf(buf, "ra H%02d%02d%02d", state->right_ascension.hours, state->right_ascension.minutes, state->right_ascension.seconds);
watch_display_string(buf, 0);
break;
case ASTRONOMY_MODE_DISPLAYING_DEC:
watch_clear_colon();
sprintf(buf, "de %3d%2d%2d", state->declination.degrees, state->declination.minutes, state->declination.seconds);
watch_display_string(buf, 0);
break;
case ASTRONOMY_MODE_DISPLAYING_DIST:
if (state->distance >= 0.00668456) {
// if >= 1,000,000 kilometers (all planets), we display distance in AU.
sprintf(buf, "diAU%6d", (uint16_t)round(state->distance * 100));
} else {
// otherwise distance in kilometers fits in 6 digits. This mode will only happen for Luna.
sprintf(buf, "di K%6ld", (uint32_t)round(state->distance * 149597871.0));
}
watch_display_string(buf, 0);
break;
case ASTRONOMY_MODE_NUM_MODES:
// this case does not happen, but we need it to silence a warning.
break;
}
}
void astronomy_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(astronomy_state_t));
memset(*context_ptr, 0, sizeof(astronomy_state_t));
}
}
void astronomy_face_activate(void *context) {
astronomy_state_t *state = (astronomy_state_t *)context;
movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1);
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;
state->latitude_radians = astro_degrees_to_radians(lat);
state->longitude_radians = astro_degrees_to_radians(lon);
movement_request_tick_frequency(4);
}
bool astronomy_face_loop(movement_event_t event, void *context) {
astronomy_state_t *state = (astronomy_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
case EVENT_TICK:
_astronomy_face_update(event, state);
break;
case EVENT_ALARM_BUTTON_UP:
switch (state->mode) {
case ASTRONOMY_MODE_SELECTING_BODY:
// advance to next celestial body (move to calculations with a long press)
state->active_body_index = (state->active_body_index + 1) % NUM_AVAILABLE_BODIES;
break;
case ASTRONOMY_MODE_CALCULATING:
// ignore button press during calculations
break;
case ASTRONOMY_MODE_DISPLAYING_DIST:
// at last mode, wrap around
state->mode = ASTRONOMY_MODE_DISPLAYING_ALT;
break;
default:
// otherwise, advance to next mode
state->mode++;
break;
}
_astronomy_face_update(event, state);
break;
case EVENT_ALARM_LONG_PRESS:
if (state->mode == ASTRONOMY_MODE_SELECTING_BODY) {
// celestial body selected! this triggers a calculation in the update method.
state->mode = ASTRONOMY_MODE_CALCULATING;
movement_request_tick_frequency(1);
_astronomy_face_update(event, state);
} else if (state->mode != ASTRONOMY_MODE_CALCULATING) {
// in all modes except "doing a calculation", return to the selection screen.
state->mode = ASTRONOMY_MODE_SELECTING_BODY;
movement_request_tick_frequency(4);
_astronomy_face_update(event, state);
}
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
case EVENT_LOW_ENERGY_UPDATE:
// TODO?
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void astronomy_face_resign(void *context) {
astronomy_state_t *state = (astronomy_state_t *)context;
state->mode = ASTRONOMY_MODE_SELECTING_BODY;
}

View File

@@ -0,0 +1,109 @@
/*
* 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 ASTRONOMY_FACE_H_
#define ASTRONOMY_FACE_H_
/*
* ASTRONOMY face
*
* The Astronomy watch face is among the most complex watch faces in the
* Movement collection. It allows you to calculate the locations of celestial
* bodies in the sky, as well as distance in astronomical units (or, in the
* case of the Moon, distance in kilometers).
*
* When you arrive at the Astronomy watch face, youll see its name (“Astro”)
* and an animation of two objects orbiting each other. You will also see “SO”
* (for Sol) flashing in the top left. The flashing letters indicate the
* currently selected celestial body. Short press Alarm to advance through
* the available celestial bodies:
*
* SO - Sol, the sun
* ME - Mercury
* VE - Venus
* LU - Luna, the Earths moon
* MA - Mars
* JU - Jupiter
* SA - Saturn
* UR - Uranus
* NE - Neptune
*
* Once youve selected the celestial body whose parameters you wish to
* calculate, long press the Alarm button and release it. The letter “C” will
* flash while the calculation is performed.
*
* When the calculation is complete, the screen will display the altitude
* (“aL”) of the celestial body. You can cycle through the available parameters
* with repeated short presses on the Alarm button:
*
* aL - Altitude (in degrees), the elevation over the horizon. If negative, it is below the horizon.
* aZ - Azimuth (in degrees), the cardinal direction relative to true north.
* rA - Right Ascension (in hours/minutes/seconds)
* dE - Declination (in degrees/minutes/seconds)
* di - Distance (the digits in the top right will display either aU for astronomical units, or K for kilometers)
*
* Long press on the Alarm button to select another celestial body.
*/
#include "movement.h"
#include "astrolib.h"
typedef enum {
ASTRONOMY_MODE_SELECTING_BODY = 0,
ASTRONOMY_MODE_CALCULATING,
ASTRONOMY_MODE_DISPLAYING_ALT,
ASTRONOMY_MODE_DISPLAYING_AZI,
ASTRONOMY_MODE_DISPLAYING_RA,
ASTRONOMY_MODE_DISPLAYING_DEC,
ASTRONOMY_MODE_DISPLAYING_DIST,
ASTRONOMY_MODE_NUM_MODES
} astronomy_mode_t;
typedef struct {
astronomy_mode_t mode;
uint8_t active_body_index;
uint8_t animation_state;
double latitude_radians; // this is the user location
double longitude_radians; // but in radians
astro_angle_hms_t right_ascension;
astro_angle_dms_t declination;
double altitude; // in decimal degrees
double azimuth; // in decimal degrees
double distance; // in AU
} astronomy_state_t;
void astronomy_face_setup(uint8_t watch_face_index, void ** context_ptr);
void astronomy_face_activate(void *context);
bool astronomy_face_loop(movement_event_t event, void *context);
void astronomy_face_resign(void *context);
#define astronomy_face ((const watch_face_t){ \
astronomy_face_setup, \
astronomy_face_activate, \
astronomy_face_loop, \
astronomy_face_resign, \
NULL, \
})
#endif // ASTRONOMY_FACE_H_

View File

@@ -0,0 +1,102 @@
/*
* 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 "blinky_face.h"
#include "watch.h"
void blinky_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(blinky_face_state_t));
memset(*context_ptr, 0, sizeof(blinky_face_state_t));
}
}
void blinky_face_activate(void *context) {
blinky_face_state_t *state = (blinky_face_state_t *)context;
state->active = false;
}
static void _blinky_face_update_lcd(blinky_face_state_t *state) {
char buf[11];
const char colors[][7] = {" red ", " Green", " Yello"};
sprintf(buf, "BL %c%s", state->fast ? 'F' : 'S', colors[state->color]);
watch_display_string(buf, 0);
}
bool blinky_face_loop(movement_event_t event, void *context) {
blinky_face_state_t *state = (blinky_face_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
_blinky_face_update_lcd(state);
break;
case EVENT_LIGHT_BUTTON_DOWN:
if (!state->active) {
state->color = (state->color + 1) % 3;
_blinky_face_update_lcd(state);
}
break;
case EVENT_ALARM_BUTTON_UP:
if (!state->active) {
state->active = true;
watch_clear_display();
movement_request_tick_frequency(state->fast ? 8 : 2);
} else {
state->active = false;
watch_set_led_off();
_blinky_face_update_lcd(state);
}
break;
case EVENT_ALARM_LONG_PRESS:
if (!state->active) {
state->fast = !state->fast;
_blinky_face_update_lcd(state);
}
break;
case EVENT_TICK:
if (state->active) {
if (event.subsecond % 2 == 0) watch_set_led_off();
else if (state->color == 0) watch_set_led_red();
else if (state->color == 1) watch_set_led_green();
else watch_set_led_yellow();
}
break;
case EVENT_TIMEOUT:
if (!state->active) movement_move_to_face(0);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void blinky_face_resign(void *context) {
(void) context;
watch_set_led_off();
}

View File

@@ -0,0 +1,75 @@
/*
* 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 BLINKY_FACE_H_
#define BLINKY_FACE_H_
/*
* BLINKY LIGHT face
*
* The blinky light watch face was designed as a tutorial for making a watch
* face in Movement, but it actually might be useful to have a blinking light
* in a pinch.
*
* The screen displays the name of the watch face (”BL”), as well as an S at
* the top right for slow blink or an F for fast blink. The bottom line selects
* the color: green, red or yellow. You can change the speed of the blinking
* light by pressing the Alarm button, and change the color with the Light
* button. A long press on the Alarm button starts the blinking light, and
* another long press stops it.
*
* Note that this will chew through your battery! The green LED uses about
* 450µA at full brightness, which is 45 times the normal power consumption of
* the watch. The red LED is an order of magnitude less efficient (4500 µA),
* and the yellow setting lights both LEDs, which chews through nearly
* 5 milliamperes. This means that one hour of yellow blinking is likely to
* eat up between 2 and 3 percent of the batterys usable life!
*
* Still, if you need to signal your location to someone in a dark forest,
* this watch face could come in handy. Just try to use the green LED as much
* as you can.
*/
#include "movement.h"
typedef struct {
bool active;
bool fast;
uint8_t color;
} blinky_face_state_t;
void blinky_face_setup(uint8_t watch_face_index, void ** context_ptr);
void blinky_face_activate(void *context);
bool blinky_face_loop(movement_event_t event, void *context);
void blinky_face_resign(void *context);
#define blinky_face ((const watch_face_t){ \
blinky_face_setup, \
blinky_face_activate, \
blinky_face_loop, \
blinky_face_resign, \
NULL, \
})
#endif // BLINKY_FACE_H_

View File

@@ -0,0 +1,204 @@
/*
* MIT License
*
* Copyright (c) 2023 Bernd Plontsch
*
* 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 "breathing_face.h"
#include "watch.h"
typedef struct {
uint8_t current_stage;
bool sound_on;
} breathing_state_t;
static void beep_in (void);
static void beep_in_hold (void);
static void beep_out (void);
static void beep_out_hold (void);
void breathing_face_setup(uint8_t watch_face_index, void ** context_ptr) {
// These next two lines just silence the compiler warnings associated with unused parameters.
// We have no use for the settings or the watch_face_index, so we make that explicit here.
(void) watch_face_index;
// At boot, context_ptr will be NULL indicating that we don't have anyplace to store our context.
if (*context_ptr == NULL) {
// in this case, we allocate an area of memory sufficient to store the stuff we need to track.
*context_ptr = malloc(sizeof(breathing_state_t));
}
}
void breathing_face_activate(void *context) {
// same as above: silence the warning, we don't need to check the settings.
// we do however need to set some things in our context. Here we cast it to the correct type...
breathing_state_t *state = (breathing_state_t *)context;
// ...and set the initial state of our watch face.
state->current_stage = 0;
state->sound_on = true;
}
const int NOTE_LENGTH = 80;
void beep_in (void) {
const watch_buzzer_note_t notes[] = {
BUZZER_NOTE_C4,
BUZZER_NOTE_D4,
BUZZER_NOTE_E4,
};
const uint16_t durations[] = {
NOTE_LENGTH,
NOTE_LENGTH,
NOTE_LENGTH
};
for(size_t i = 0, count = sizeof(notes) / sizeof(notes[0]); i < count; i++) {
watch_buzzer_play_note(notes[i], durations[i]);
}
}
void beep_in_hold (void) {
const watch_buzzer_note_t notes[] = {
BUZZER_NOTE_E4,
BUZZER_NOTE_REST,
BUZZER_NOTE_E4,
};
const uint16_t durations[] = {
NOTE_LENGTH,
NOTE_LENGTH * 2,
NOTE_LENGTH,
};
for(size_t i = 0, count = sizeof(notes) / sizeof(notes[0]); i < count; i++) {
watch_buzzer_play_note(notes[i], durations[i]);
}
}
void beep_out (void) {
const watch_buzzer_note_t notes[] = {
BUZZER_NOTE_E4,
BUZZER_NOTE_D4,
BUZZER_NOTE_C4,
};
const uint16_t durations[] = {
NOTE_LENGTH,
NOTE_LENGTH,
NOTE_LENGTH,
};
for(size_t i = 0, count = sizeof(notes) / sizeof(notes[0]); i < count; i++) {
watch_buzzer_play_note(notes[i], durations[i]);
}
}
void beep_out_hold (void) {
const watch_buzzer_note_t notes[] = {
BUZZER_NOTE_C4,
BUZZER_NOTE_REST * 2,
BUZZER_NOTE_C4,
};
const uint16_t durations[] = {
NOTE_LENGTH,
NOTE_LENGTH,
NOTE_LENGTH,
};
for(size_t i = 0, count = sizeof(notes) / sizeof(notes[0]); i < count; i++) {
watch_buzzer_play_note(notes[i], durations[i]);
}
}
bool breathing_face_loop(movement_event_t event, void *context) {
breathing_state_t *state = (breathing_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
case EVENT_TICK:
if (state->sound_on == true) {
watch_set_indicator(WATCH_INDICATOR_BELL);
} else {
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
switch (state->current_stage)
{
case 0: { watch_display_string("Breath", 4); if (state->sound_on) beep_in(); } break;
case 1: watch_display_string("In 3", 4); break;
case 2: watch_display_string("In 2", 4); break;
case 3: watch_display_string("In 1", 4); break;
case 4: { watch_display_string("Hold 4", 4); if (state->sound_on) beep_in_hold(); } break;
case 5: watch_display_string("Hold 3", 4); break;
case 6: watch_display_string("Hold 2", 4); break;
case 7: watch_display_string("Hold 1", 4); break;
case 8: { watch_display_string("Ou t 4", 4); if (state->sound_on) beep_out(); } break;
case 9: watch_display_string("Ou t 3", 4); break;
case 10: watch_display_string("Ou t 2", 4); break;
case 11: watch_display_string("Ou t 1", 4); break;
case 12: { watch_display_string("Hold 4", 4); if (state->sound_on) beep_out_hold(); } break;
case 13: watch_display_string("Hold 3", 4); break;
case 14: watch_display_string("Hold 2", 4); break;
case 15: watch_display_string("Hold 1", 4); break;
default:
break;
}
// and increment it so that it will update on the next tick.
state->current_stage = (state->current_stage + 1) % 16;
break;
case EVENT_ALARM_BUTTON_UP:
state->sound_on = !state->sound_on;
if (state->sound_on == true) {
watch_set_indicator(WATCH_INDICATOR_BELL);
} else {
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
break;
case EVENT_LOW_ENERGY_UPDATE:
// This low energy mode update occurs once a minute, if the watch face is in the
// foreground when Movement enters low energy mode. We have the option of supporting
// this mode, but since our watch face animates once a second, the "Hello there" face
// isn't very useful in this mode. So we choose not to support it. (continued below)
break;
case EVENT_TIMEOUT:
// ... Instead, we respond to the timeout event. This event happens after a configurable
// interval on screen (1-30 minutes). The watch will give us this event as a chance to
// resign control if we want to, and in this case, we do.
// This function will return the watch to the first screen (usually a simple clock),
// and it will do it long before the watch enters low energy mode. This ensures we
// won't be on screen, and thus opts us out of getting the EVENT_LOW_ENERGY_UPDATE above.
// movement_move_to_face(0);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void breathing_face_resign(void *context) {
// our watch face, like most watch faces, has nothing special to do when resigning.
// watch faces that enable a peripheral or interact with a sensor may want to turn it off here.
(void) context;
}

View File

@@ -0,0 +1,54 @@
/*
* MIT License
*
* Copyright (c) 2023 Bernd Plontsch
*
* 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 BREATHING_FACE_H_
#define BREATHING_FACE_H_
/*
* BOXED BREATHING face
*
* Breathing is a complication for guiding boxed breathing sessions.
* Boxed breathing is a technique to help you stay calm and improve
* concentration in stressful situations.
*
* Usage: Timed messages will cycle as long as this face is active.
* Press ALARM to toggle sound.
*/
#include "movement.h"
void breathing_face_setup(uint8_t watch_face_index, void ** context_ptr);
void breathing_face_activate(void *context);
bool breathing_face_loop(movement_event_t event, void *context);
void breathing_face_resign(void *context);
#define breathing_face ((const watch_face_t){ \
breathing_face_setup, \
breathing_face_activate, \
breathing_face_loop, \
breathing_face_resign, \
NULL, \
})
#endif // BREATHING_FACE_H_

View File

@@ -0,0 +1,464 @@
/*
* 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(uint8_t watch_face_index, void ** context_ptr) {
(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(void *context) {
(void) context;
movement_request_tick_frequency(TICK_FREQ);
}
bool butterfly_game_face_loop(movement_event_t event, 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);
}
}
void butterfly_game_face_resign(void *context) {
(void) context;
// handle any cleanup before your watch face goes off-screen.
}

View 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(uint8_t watch_face_index, void ** context_ptr);
void butterfly_game_face_activate(void *context);
bool butterfly_game_face_loop(movement_event_t event, void *context);
void butterfly_game_face_resign(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_

View File

@@ -0,0 +1,264 @@
/*
* MIT License
*
* Copyright (c) 2023 Ekaitz Zarraga <ekaitz@elenq.tech>
*
* 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 "couch_to_5k_face.h"
// They go: Warmup, Run, Walk, Run, Walk, Run, Walk ... , End (0)
// Time is defined in seconds
// Maybe do /10 to reduce memory usage?
// (i don't want to use floats)
// uint16_t C25K_WEEK_TEST[] = {10, 10, 10, 0};
uint16_t C25K_WEEK_1[] = {300, 60, 90, 60, 90, 60, 90, 60, 90, 60, 90, 60,
90, 60, 90, 60, 90, 0};
uint16_t C25K_WEEK_2[] = {300, 90, 120, 90, 120, 90, 120, 90, 120, 90, 120,
90, 120, 0};
uint16_t C25K_WEEK_3[] = {300, 90, 90, 180, 180, 90, 90, 180, 180, 0};
uint16_t C25K_WEEK_4[] = {300, 180, 90, 300, 150, 180, 90, 300, 0};
uint16_t C25K_WEEK_5_1[] = {300, 300, 180, 300, 180, 300, 0 };
uint16_t C25K_WEEK_5_2[] = {300, 480, 300, 480 , 0};
uint16_t C25K_WEEK_5_3[] = {300, 1200, 0};
uint16_t C25K_WEEK_6_1[] = {300, 300, 180, 480, 180, 300, 0 };
uint16_t C25K_WEEK_6_2[] = {300, 600, 180, 600 , 0};
uint16_t C25K_WEEK_6_3[] = {300, 1500, 0};
uint16_t C25K_WEEK_7[] = {300, 1500, 0};
uint16_t C25K_WEEK_8[] = {300, 1680, 0};
uint16_t C25K_WEEK_9[] = {300, 1800, 0};
#define C25K_SESSIONS_LENGTH 3*9
uint16_t *C25K_SESSIONS[C25K_SESSIONS_LENGTH];
static inline bool _finished(couch_to_5k_state_t *state){
return state->exercise_type == C25K_FINISHED;
}
static inline bool _cleared(couch_to_5k_state_t *state){
return state->timer == C25K_SESSIONS[state->session][0]
&& state->exercise == 0;
}
static inline void _next_session(couch_to_5k_state_t *state){
if (++state->session >= C25K_SESSIONS_LENGTH){
state->session = 0;
}
}
static inline void _assign_exercise_type(couch_to_5k_state_t *state){
if (state->exercise == 0){
state->exercise_type = C25K_WARMUP;
} else if (state->exercise % 2 == 1){
state->exercise_type = C25K_RUN;
} else {
state->exercise_type = C25K_WALK;
}
}
static void _next_exercise(couch_to_5k_state_t *state){
state->exercise++;
state->timer = C25K_SESSIONS[state->session][state->exercise];
// If the new timer starts in zero, it's finished
if (state->timer == 0){
movement_play_alarm_beeps(7, BUZZER_NOTE_C8);
state->exercise_type = C25K_FINISHED;
return;
}
movement_play_alarm_beeps(4, BUZZER_NOTE_A7);
_assign_exercise_type(state);
}
static void _init_session(couch_to_5k_state_t *state){
state->exercise = 0; // Restart exercise counter
state->timer = C25K_SESSIONS[state->session][state->exercise];
_assign_exercise_type(state);
}
static char *_exercise_type_to_str(exercise_type_t t){
switch (t){
case C25K_WARMUP:
return "WU";
case C25K_RUN:
return "RU";
case C25K_WALK:
return "WA";
case C25K_FINISHED:
return "--";
default:
return " ";
}
}
static void _display(couch_to_5k_state_t *state, char *buf){
// TODO only repaint needed parts
uint8_t seconds = state->timer % 60;
sprintf(buf, "%s%2d%2d%02d%02d",
_exercise_type_to_str(state->exercise_type),
(state->session + 1) % 100,
((state->timer - seconds) / 60) % 100,
seconds,
(state->exercise + 1) % 100);
watch_display_string(buf, 0);
}
void couch_to_5k_face_setup(uint8_t
watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(couch_to_5k_state_t));
memset(*context_ptr, 0, sizeof(couch_to_5k_state_t));
// Do any one-time tasks in here; the inside of this conditional
// happens only at boot.
// C25K_SESSIONS[0] = C25K_WEEK_TEST;
C25K_SESSIONS[0] = C25K_WEEK_1;
C25K_SESSIONS[1] = C25K_WEEK_1;
C25K_SESSIONS[2] = C25K_WEEK_1;
C25K_SESSIONS[3] = C25K_WEEK_2;
C25K_SESSIONS[4] = C25K_WEEK_2;
C25K_SESSIONS[5] = C25K_WEEK_2;
C25K_SESSIONS[6] = C25K_WEEK_3;
C25K_SESSIONS[7] = C25K_WEEK_3;
C25K_SESSIONS[8] = C25K_WEEK_3;
C25K_SESSIONS[9] = C25K_WEEK_4;
C25K_SESSIONS[10] = C25K_WEEK_4;
C25K_SESSIONS[11] = C25K_WEEK_4;
C25K_SESSIONS[12] = C25K_WEEK_5_1;
C25K_SESSIONS[13] = C25K_WEEK_5_2;
C25K_SESSIONS[14] = C25K_WEEK_5_3;
C25K_SESSIONS[15] = C25K_WEEK_6_1;
C25K_SESSIONS[16] = C25K_WEEK_6_2;
C25K_SESSIONS[17] = C25K_WEEK_6_3;
C25K_SESSIONS[18] = C25K_WEEK_7;
C25K_SESSIONS[19] = C25K_WEEK_7;
C25K_SESSIONS[20] = C25K_WEEK_7;
C25K_SESSIONS[21] = C25K_WEEK_8;
C25K_SESSIONS[22] = C25K_WEEK_8;
C25K_SESSIONS[23] = C25K_WEEK_8;
C25K_SESSIONS[24] = C25K_WEEK_9;
C25K_SESSIONS[25] = C25K_WEEK_9;
C25K_SESSIONS[26] = C25K_WEEK_9;
}
// Do any pin or peripheral setup here; this will be called whenever the
// watch wakes from deep sleep.
}
void couch_to_5k_face_activate(void *context) {
(void) context;
// Handle any tasks related to your watch face coming on screen.
watch_set_colon();
}
bool couch_to_5k_face_loop(movement_event_t event,
void *context) {
couch_to_5k_state_t *state = (couch_to_5k_state_t *)context;
static char buf[11];
static bool paused = true;
switch (event.event_type) {
case EVENT_ACTIVATE:
// Show your initial UI here.
movement_request_tick_frequency(1);
_init_session(state);
paused = true;
_display(state, buf);
break;
case EVENT_TICK:
if ( !paused && !_finished(state) ) {
if (state->timer == 0){
_next_exercise(state);
} else {
state->timer--;
}
}
_display(state, buf);
break;
case EVENT_LIGHT_BUTTON_UP:
// This is the next-exercise / reset button.
// When finished move to the next session and leave it paused
if ( _finished(state) ){
_next_session(state);
_init_session(state);
paused = true;
break;
}
// When paused and cleared move to next, when only paused, clear
if ( paused ) {
if ( _cleared(state) ){
_next_session(state);
}
_init_session(state);
}
break;
case EVENT_ALARM_BUTTON_UP:
if (movement_button_should_sound()) {
watch_buzzer_play_note(BUZZER_NOTE_C8, 50);
}
paused = !paused;
break;
case EVENT_TIMEOUT:
// Your watch face will receive this event after a period of
// inactivity. If it makes sense to resign,
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_sleep_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);
}
// return true if the watch can enter standby mode. Generally speaking, you
// should always return true.
// Exceptions:
// * If you are displaying a color using the low-level watch_set_led_color
// function, you should return false.
// * If you are sounding the buzzer using the low-level
// watch_set_buzzer_on function, you should return false.
// Note that if you are driving the LED or buzzer using Movement functions
// like movement_illuminate_led or movement_play_alarm, you can still
// return true. This guidance only applies to the low-level watch_
// functions.
return true;
}
void couch_to_5k_face_resign(void *context) {
(void) context;
// handle any cleanup before your watch face goes off-screen.
}

View File

@@ -0,0 +1,87 @@
/*
* MIT License
*
* Copyright (c) 2023 Ekaitz Zarraga <ekaitz@elenq.tech>
*
* 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 COUCHTO5K_FACE_H_
#define COUCHTO5K_FACE_H_
#include "movement.h"
/*
* Couch To 5k;
*
*
* The program is designed to train 3 times a week. Each training is a
* *session*. Each of the rounds you have in the training is an *exercise*.
*
* The training goes like this:
* 5min warm-up walk -> Run X minutes -> Walk Y minutes -> ... -> Stop
*
* The watch face shows it like this: The weekday indicator shows if you need
* to Warm Up (`WU`), run (`rU`), walk (`WA`) or stop (`--`).
*
* The month-day indicator shows the session you are in (from 1 to 27).
*
* The timer shows the time you have left in the exercise and the exercise you
* are doing (MM:SS:ee). When an exercise finishes you are notified with an
* alarm. When the whole session finishes, a different tone is played for a
* longer period.
*
* Pressing the ALARM button pauses/resumes the clock.
*
* Pressing the LIGHT button does nothing if the timer is not paused. When it
* is paused it clears the current session (it restarts it to the beginning)
* and if it was already cleared or the current session was finished moves to
* the next session.
*/
typedef enum {
C25K_WARMUP,
C25K_RUN,
C25K_WALK,
C25K_FINISHED
} exercise_type_t;
typedef struct {
// Anything you need to keep track of, put it here!
uint8_t session;
uint8_t exercise;
exercise_type_t exercise_type;
uint16_t timer;
} couch_to_5k_state_t;
void couch_to_5k_face_setup(uint8_t watch_face_index, void ** context_ptr);
void couch_to_5k_face_activate(void *context);
bool couch_to_5k_face_loop(movement_event_t event, void *context);
void couch_to_5k_face_resign(void *context);
#define couch_to_5k_face ((const watch_face_t){ \
couch_to_5k_face_setup, \
couch_to_5k_face_activate, \
couch_to_5k_face_loop, \
couch_to_5k_face_resign, \
NULL, \
})
#endif // COUCHTO5K_FACE_H_

View File

@@ -0,0 +1,145 @@
/*
* MIT License
*
* Copyright (c) 2022 Shogo Okamoto
*
* 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 "counter_face.h"
#include "watch.h"
void counter_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(counter_state_t));
memset(*context_ptr, 0, sizeof(counter_state_t));
counter_state_t *state = (counter_state_t *)*context_ptr;
state->beep_on = true;
}
}
void counter_face_activate(void *context) {
counter_state_t *state = (counter_state_t *)context;
if (state->beep_on) {
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
}
}
bool counter_face_loop(movement_event_t event, void *context) {
counter_state_t *state = (counter_state_t *)context;
switch (event.event_type) {
case EVENT_ALARM_BUTTON_UP:
watch_buzzer_abort_sequence(); //abort running buzzer sequence when counting fast
state->counter_idx++; // increment counter index
if (state->counter_idx>99) { //0-99
state->counter_idx=0;//reset counter index
}
print_counter(state);
if (state->beep_on) {
beep_counter(state);
}
break;
case EVENT_LIGHT_LONG_PRESS:
watch_buzzer_abort_sequence();
state->beep_on = !state->beep_on;
if (state->beep_on) {
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
} else {
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
}
break;
case EVENT_ALARM_LONG_PRESS:
state->counter_idx=0; // reset counter index
print_counter(state);
break;
case EVENT_ACTIVATE:
print_counter(state);
break;
case EVENT_TIMEOUT:
// ignore timeout
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
// beep counter index times
void beep_counter(counter_state_t *state) {
int low_count = state->counter_idx/5;
int high_count = state->counter_idx - low_count * 5;
static int8_t sound_seq[15];
memset(sound_seq, 0, 15);
int i = 0;
if (low_count > 0) {
sound_seq[i] = BUZZER_NOTE_A6;
i++;
sound_seq[i] = 3;
i++;
sound_seq[i] = BUZZER_NOTE_REST;
i++;
sound_seq[i] = 6;
i++;
if (low_count > 1) {
sound_seq[i] = -2;
i++;
sound_seq[i] = low_count-1;
i++;
}
sound_seq[i] = BUZZER_NOTE_REST;
i++;
sound_seq[i] = 6;
i++;
}
if (high_count > 0) {
sound_seq[i] = BUZZER_NOTE_B6;
i++;
sound_seq[i] = 3;
i++;
sound_seq[i] = BUZZER_NOTE_REST;
i++;
sound_seq[i] = 6;
i++;
}
if (high_count > 1) {
sound_seq[i] = -2;
i++;
sound_seq[i] = high_count-1;
}
watch_buzzer_play_sequence((int8_t *)sound_seq, NULL);
}
// print counter index at the center of display.
void print_counter(counter_state_t *state) {
char buf[14];
sprintf(buf, "CO %02d", state->counter_idx); // center of LCD display
watch_display_string(buf, 0);
}
void counter_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,63 @@
/*
* MIT License
*
* Copyright (c) 2022 Shogo Okamoto
*
* 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 COUNTER_FACE_H_
#define COUNTER_FACE_H_
/*
* COUNTER face
*
* Counter face is designed to count the number of running laps during exercises.
*
* Usage:
* Short-press ALARM to increment the counter (loops at 99)
* Long-press ALARM to reset the counter.
* Long-press LIGHT to toggle sound.
*/
#include "movement.h"
typedef struct {
uint8_t counter_idx;
bool beep_on;
} counter_state_t;
void counter_face_setup(uint8_t watch_face_index, void ** context_ptr);
void counter_face_activate(void *context);
bool counter_face_loop(movement_event_t event, void *context);
void counter_face_resign(void *context);
void print_counter(counter_state_t *state);
void beep_counter(counter_state_t *state);
#define counter_face ((const watch_face_t){ \
counter_face_setup, \
counter_face_activate, \
counter_face_loop, \
counter_face_resign, \
NULL, \
})
#endif // COUNTER_FACE_H_

View File

@@ -0,0 +1,143 @@
/*
* MIT License
*
* Copyright (c) 2022 Mikhail Svarichevsky
*
* 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 "databank_face.h"
#include "watch.h"
#include "watch_private_display.h"
const char *pi_data[] = {
"PI", "314159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848111745028410270193852110555964462294895493038196442",
"S ", "9192631770",
"31", "2147483648",
"32", "4294967296",
"63", "9223372036854775808",
"64", "18446744073709551616",
};
//we show 6 characters per screen
const int databank_num_pages = (sizeof(pi_data) / sizeof(char*) / 2);
struct {
uint8_t current_word;
uint8_t databank_page;
bool animating;
} databank_state;
void databank_face_setup(uint8_t watch_face_index, void ** context_ptr) {
// These next two lines just silence the compiler warnings associated with unused parameters.
// We have no use for the settings or the watch_face_index, so we make that explicit here.
(void) context_ptr;
(void) watch_face_index;
// At boot, context_ptr will be NULL indicating that we don't have anyplace to store our context.
}
void databank_face_activate(void *context) {
// same as above: silence the warning, we don't need to check the settings.
(void) context;
// we do however need to set some things in our context. Here we cast it to the correct type...
databank_state.current_word = 0;
databank_state.animating = true;
}
static void display()
{
char buf[14];
int page = databank_state.current_word;
sprintf(buf, "%s%2d", pi_data[databank_state.databank_page * 2 + 0], page);
watch_display_string(buf, 0);
bool data_ended = false;
for (int i = 0; i < 6; i++) {
if (pi_data[databank_state.databank_page * 2 + 1][page * 6 + i] == 0) {
data_ended = true;
}
if (!data_ended) {
watch_display_character(pi_data[databank_state.databank_page * 2 + 1][page * 6 + i], 4 + i);
} else {
// only 6 digits per page
watch_display_character(' ', 4 + i);
}
}
}
bool databank_face_loop(movement_event_t event, void *context) {
(void) context;
int max_words = (strlen(pi_data[databank_state.databank_page * 2 + 1]) - 1) / 6 + 1;
switch (event.event_type) {
case EVENT_ACTIVATE:
display();
case EVENT_TICK:
break;
case EVENT_LIGHT_BUTTON_UP:
databank_state.current_word = (databank_state.current_word + max_words - 1) % max_words;
display();
break;
case EVENT_LIGHT_LONG_PRESS:
databank_state.databank_page = (databank_state.databank_page + databank_num_pages - 1) % databank_num_pages;
databank_state.current_word = 0;
display();
break;
case EVENT_ALARM_LONG_PRESS:
databank_state.databank_page = (databank_state.databank_page + 1) % databank_num_pages;
databank_state.current_word = 0;
display();
break;
case EVENT_ALARM_BUTTON_UP:
databank_state.current_word = (databank_state.current_word + 1) % max_words;
display();
break;
case EVENT_LOW_ENERGY_UPDATE:
// This low energy mode update occurs once a minute, if the watch face is in the
// foreground when Movement enters low energy mode. We have the option of supporting
// this mode, but since our watch face animates once a second, the "Hello there" face
// isn't very useful in this mode. So we choose not to support it. (continued below)
break;
case EVENT_TIMEOUT:
// ... Instead, we respond to the timeout event. This event happens after a configurable
// interval on screen (1-30 minutes). The watch will give us this event as a chance to
// resign control if we want to, and in this case, we do.
// This function will return the watch to the first screen (usually a simple clock),
// and it will do it long before the watch enters low energy mode. This ensures we
// won't be on screen, and thus opts us out of getting the EVENT_LOW_ENERGY_UPDATE above.
movement_move_to_face(0);
break;
case EVENT_LIGHT_BUTTON_DOWN:
// don't light up every time light is hit
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void databank_face_resign(void *context) {
// our watch face, like most watch faces, has nothing special to do when resigning.
// watch faces that enable a peripheral or interact with a sensor may want to turn it off here.
(void) context;
}

View File

@@ -0,0 +1,60 @@
/*
* 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 DATABANK_FACE_H_
#define DATABANK_FACE_H_
/*
* DATABANK face
*
* Displays some pre-defined data that you might want to remember
* Math constants, birthdays, phone numbers...
*
* Usage: Edit the global variable `pi_data` in "databank_face.c"
* to the define the data that will be displayed. Each "item" contains
* a two-letter label (using the day-of-week display), then a longer
* string that will be displayed one "word" (six characters) at a time.
*
* Short-press ALARM to display the next word.
* Short-press LIGHT to display the previous word.
* Long-press ALARM to display the next item.
* Long-press LIGHT to display the previous item.
*/
#include "movement.h"
void databank_face_setup(uint8_t watch_face_index, void ** context_ptr);
void databank_face_activate(void *context);
bool databank_face_loop(movement_event_t event, void *context);
void databank_face_resign(void *context);
#define databank_face ((const watch_face_t){ \
databank_face_setup, \
databank_face_activate, \
databank_face_loop, \
databank_face_resign, \
NULL, \
})
#endif // DATABANK_FACE_H_

View File

@@ -0,0 +1,270 @@
/*
* 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 "day_one_face.h"
#include "watch.h"
#include "watch_utility.h"
static uint32_t _day_one_face_juliandaynum(uint16_t year, uint16_t month, uint16_t day) {
// from here: https://en.wikipedia.org/wiki/Julian_day#Julian_day_number_calculation
return (1461 * (year + 4800 + (month - 14) / 12)) / 4 + (367 * (month - 2 - 12 * ((month - 14) / 12))) / 12 - (3 * ((year + 4900 + (month - 14) / 12) / 100))/4 + day - 32075;
}
static void _day_one_face_update(day_one_state_t *state) {
char buf[15];
watch_date_time_t date_time = watch_rtc_get_date_time();
uint32_t julian_date = _day_one_face_juliandaynum(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR, date_time.unit.month, date_time.unit.day);
uint32_t julian_birthdate = _day_one_face_juliandaynum(state->birth_year, state->birth_month, state->birth_day);
if (julian_date < julian_birthdate) {
sprintf(buf, "DA %6lu", julian_birthdate - julian_date);
} else {
sprintf(buf, "DA %6lu", julian_date - julian_birthdate);
}
watch_display_string(buf, 0);
}
static void _day_one_face_abort_quick_cycle(day_one_state_t *state) {
if (state->quick_cycle) {
state->quick_cycle = false;
movement_request_tick_frequency(4);
}
}
static void _day_one_face_increment(day_one_state_t *state) {
state->birthday_changed = true;
switch (state->current_page) {
case PAGE_YEAR:
state->birth_year = state->birth_year + 1;
if (state->birth_year > 2080) state->birth_year = 1900;
break;
case PAGE_MONTH:
state->birth_month = (state->birth_month % 12) + 1;
break;
case PAGE_DAY:
state->birth_day = (state->birth_day % watch_utility_days_in_month(state->birth_month, state->birth_year)) + 1;
break;
default:
break;
}
}
void day_one_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(day_one_state_t));
memset(*context_ptr, 0, sizeof(day_one_state_t));
movement_birthdate_t movement_birthdate = (movement_birthdate_t) watch_get_backup_data(2);
if (movement_birthdate.reg == 0) {
// if birth date is totally blank, set a reasonable starting date. this works well for anyone under 63, but
// you can keep pressing to go back to 1900; just pass the year 2080. also picked this date because if you
// set it to 1959-01-02, it counts up from the launch of Luna-1, the first spacecraft to leave the well.
movement_birthdate.bit.year = 1959;
movement_birthdate.bit.month = 1;
movement_birthdate.bit.day = 1;
watch_store_backup_data(movement_birthdate.reg, 2);
}
}
}
void day_one_face_activate(void *context) {
day_one_state_t *state = (day_one_state_t *)context;
state->current_page = PAGE_DISPLAY;
state->quick_cycle = false;
state->ticks = 0;
// fetch the user's birth date from the birthday register.
movement_birthdate_t movement_birthdate = (movement_birthdate_t) watch_get_backup_data(2);
state->birth_year = movement_birthdate.bit.year;
state->birth_month = movement_birthdate.bit.month;
state->birth_day = movement_birthdate.bit.day;
}
bool day_one_face_loop(movement_event_t event, void *context) {
day_one_state_t *state = (day_one_state_t *)context;
char buf[9];
switch (event.event_type) {
case EVENT_ACTIVATE:
_day_one_face_update(state);
break;
case EVENT_LOW_ENERGY_UPDATE:
case EVENT_TICK:
if (state->quick_cycle) {
if (HAL_GPIO_BTN_ALARM_read()) {
_day_one_face_increment(state);
} else {
_day_one_face_abort_quick_cycle(state);
}
}
switch (state->current_page) {
// if in settings mode, update whatever the current page is
case PAGE_YEAR:
watch_display_string("YR ", 0);
if (event.subsecond % 2) {
sprintf(buf, "%4d", state->birth_year);
watch_display_string(buf, 4);
}
break;
case PAGE_MONTH:
watch_display_string("MO ", 0);
if (event.subsecond % 2) {
sprintf(buf, "%2d", state->birth_month);
watch_display_string(buf, 4);
}
break;
case PAGE_DAY:
watch_display_string("DA ", 0);
if (event.subsecond % 2) {
sprintf(buf, "%2d", state->birth_day);
watch_display_string(buf, 6);
}
break;
// otherwise, check if we have to update. the display only needs to change at midnight!
case PAGE_DISPLAY: {
watch_date_time_t date_time = watch_rtc_get_date_time();
if (date_time.unit.hour == 0 && date_time.unit.minute == 0 && date_time.unit.second == 0) {
_day_one_face_update(state);
}
break;}
case PAGE_DATE:
if (state->ticks > 0) {
state->ticks--;
} else {
state->current_page = PAGE_DISPLAY;
_day_one_face_update(state);
}
break;
default:
break;
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
// only illuminate if we're in display mode
switch (state->current_page) {
case PAGE_DISPLAY:
// fall through
case PAGE_DATE:
movement_illuminate_led();
break;
default:
break;
}
break;
case EVENT_LIGHT_BUTTON_UP:
// otherwise use the light button to advance settings pages.
switch (state->current_page) {
case PAGE_YEAR:
// fall through
case PAGE_MONTH:
// fall through
case PAGE_DAY:
// go to next setting page...
state->current_page = (state->current_page + 1) % 4;
if (state->current_page == PAGE_DISPLAY) {
// ...unless we've been pushed back to display mode.
movement_request_tick_frequency(1);
// force display since it normally won't update til midnight.
_day_one_face_update(state);
}
break;
default:
break;
}
break;
case EVENT_ALARM_BUTTON_UP:
// if we are on a settings page, increment whatever value we're setting.
switch (state->current_page) {
case PAGE_YEAR:
// fall through
case PAGE_MONTH:
// fall through
case PAGE_DAY:
_day_one_face_abort_quick_cycle(state);
_day_one_face_increment(state);
break;
case PAGE_DISPLAY:
state->current_page = PAGE_DATE;
sprintf(buf, "%04d%02d%02d", state->birth_year % 10000, state->birth_month % 100, state->birth_day % 100);
watch_display_string(buf, 2);
state->ticks = 2;
break;
default:
break;
}
break;
case EVENT_ALARM_LONG_PRESS:
// if we aren't already in settings mode, put us there.
switch (state->current_page) {
case PAGE_DISPLAY:
state->current_page++;
movement_request_tick_frequency(4);
break;
case PAGE_YEAR:
// fall through
case PAGE_MONTH:
// fall through
case PAGE_DAY:
state->quick_cycle = true;
movement_request_tick_frequency(8);
break;
default:
break;
}
break;
case EVENT_ALARM_LONG_UP:
_day_one_face_abort_quick_cycle(state);
break;
case EVENT_TIMEOUT:
_day_one_face_abort_quick_cycle(state);
// return home if we're on a settings page (this saves our changes when we resign).
if (state->current_page != PAGE_DISPLAY) {
movement_move_to_face(0);
}
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void day_one_face_resign(void *context) {
day_one_state_t *state = (day_one_state_t *)context;
// if the user changed their birth date, store it to the birth date register
if (state->birthday_changed) {
day_one_state_t *state = (day_one_state_t *)context;
movement_birthdate_t movement_birthdate = (movement_birthdate_t) watch_get_backup_data(2);
movement_birthdate.bit.year = state->birth_year;
movement_birthdate.bit.month = state->birth_month;
movement_birthdate.bit.day = state->birth_day;
watch_store_backup_data(movement_birthdate.reg, 2);
state->birthday_changed = false;
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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 DAY_ONE_FACE_H_
#define DAY_ONE_FACE_H_
/*
* DAY ONE face
*
* This watch face displays the number of days since or until a given date.
* It was originally designed to display the number of days youve been alive,
* but technically it can count up from any date in the 20th century or the
* 21st century, so far.
*
* Long press on the Alarm button to enter customization mode. The text “YR”
* will appear, and will allow you to set the year starting from 1959. Press
* Alarm repeatedly to advance the year. If your birthday is before 1959,
* advance beyond the current year and it will wrap around to 1900.
*
* Once you have set the year, press Light to set the month (“MO”) and
* day (“DA”), advancing the value by pressing Alarm repeatedly.
*
* Note that at this time, the Day One face does not display the sleep
* indicator in sleep mode, which may make the watch appear to be
* unresponsive in sleep mode. You can still press the Alarm button to
* wake the watch. This UI quirk will be addressed in a future update.
*/
#include "movement.h"
typedef enum {
PAGE_DISPLAY,
PAGE_YEAR,
PAGE_MONTH,
PAGE_DAY,
PAGE_DATE
} day_one_page_t;
typedef struct {
day_one_page_t current_page;
uint16_t birth_year;
uint8_t birth_month;
uint8_t birth_day;
bool birthday_changed;
bool quick_cycle;
uint8_t ticks;
} day_one_state_t;
void day_one_face_setup(uint8_t watch_face_index, void ** context_ptr);
void day_one_face_activate(void *context);
bool day_one_face_loop(movement_event_t event, void *context);
void day_one_face_resign(void *context);
#define day_one_face ((const watch_face_t){ \
day_one_face_setup, \
day_one_face_activate, \
day_one_face_loop, \
day_one_face_resign, \
NULL, \
})
#endif // DAY_ONE_FACE_H_

View File

@@ -0,0 +1,626 @@
/*
* MIT License
*
* Copyright (c) 2023-2024 Konrad Rieck
*
* 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.
*/
/*
* # Deadline Face
*
* This is a watch face for tracking deadlines. It draws inspiration from
* other watch faces of the project but focuses on keeping track of
* deadlines. You can enter and monitor up to four different deadlines by
* providing their respective date and time. The face has two modes:
* *running mode* and *settings mode*.
*
* ## Running Mode
*
* When the watch face is activated, it defaults to running mode. The top
* right corner shows the current deadline number, and the main display
* presents the time left until the deadline. The format of the display
* varies depending on the remaining time.
*
* - When less than a day is left, the display shows the remaining hours,
* minutes, and seconds in the form `HH:MM:SS`.
*
* - When less than a month is left, the display shows the remaining days
* and hours in the form `DD:HH` with the unit `dy` for days.
*
* - When less than a year is left, the display shows the remaining months
* and days in the form `MM:DD` with the unit `mo` for months.
*
* - When more than a year is left, the years and months are displayed in
* the form `YY:MM` with the unit `yr` for years.
*
* - When a deadline has passed in the last 24 hours, the display shows
* `over` to indicate that the deadline has just recently been reached.
*
* - When no deadline is set for a particular slot, or if a deadline has
* already passed by more than 24 hours, `--:--` is displayed.
*
* The user can navigate in running mode using the following buttons:
*
* - The *alarm button* moves the next deadline. There are currently four
* slots available for deadlines. When the last slot has been reached,
* pressing the button moves to the first slot.
*
* - A *long press* on the *alarm button* activates settings mode and
* enables configuring the currently selected deadline.
*
* - A *long press* on the *light button* activates a deadline alarm. The
* bell icon is displayed, and the alarm will ring upon reaching any of
* the deadlines set. It is important to note that the watch will not
* enter low-energy sleep mode while the alarm is enabled.
*
*
* ## Settings Mode
*
* In settings mode, the currently selected slot for a deadline can be
* configured by providing the date and the time. Like running mode, the
* top right corner of the display indicates the current deadline number.
* The main display shows the date and, on the next page, the time to be
* configured.
*
* The user can use the following buttons in settings mode.
*
* - The *light button* navigates through the different date and time
* settings, going from year, month, day, hour, to minute. The selected
* position is blinking.
*
* - A *long press* on the light button resets the date and time to the next
* day at midnight. This is the default deadline.
*
* - The *alarm button* increments the currently selected position. A *long
* press* on the *alarm button* changes the value faster.
*
* - The *mode button* exists setting mode and returns to *running mode*.
* Here the selected deadline slot can be changed.
*
*/
#include <stdlib.h>
#include <string.h>
#include "deadline_face.h"
#include "watch.h"
#include "watch_utility.h"
#define SETTINGS_NUM (5)
const char settings_titles[SETTINGS_NUM][3] = { "YR", "MO", "DA", "HR", "M1" };
/* Local functions */
static void _running_init(deadline_state_t *state);
static bool _running_loop(movement_event_t event, void *context);
static void _running_display(movement_event_t event, deadline_state_t *state);
static void _setting_init(deadline_state_t *state);
static bool _setting_loop(movement_event_t event, void *context);
static void _setting_display(movement_event_t event, deadline_state_t *state, watch_date_time_t date);
/* Utility functions */
static void _background_alarm_play(deadline_state_t *state);
static void _background_alarm_schedule(deadline_state_t *state);
static void _background_alarm_cancel(deadline_state_t *state);
static void _increment_date(deadline_state_t *state, watch_date_time_t date_time);
static inline void _change_tick_freq(uint8_t freq, deadline_state_t *state);
static inline bool _is_leap(int16_t y);
static inline int _days_in_month(int16_t mpnth, int16_t y);
static inline unsigned int _mod(int a, int b);
static inline void _beep_button();
static inline void _beep_enable();
static inline void _beep_disable();
static inline void _reset_deadline(deadline_state_t *state);
/* Check for leap year */
static inline bool _is_leap(int16_t y)
{
y += 1900;
return !(y % 4) && ((y % 100) || !(y % 400));
}
/* Modulo function */
static inline unsigned int _mod(int a, int b)
{
int r = a % b;
return r < 0 ? r + b : r;
}
/* Return days in month */
static inline int _days_in_month(int16_t month, int16_t year)
{
uint8_t days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
month = _mod(month - 1, 12);
if (month == 1 && _is_leap(year)) {
return days[month] + 1;
} else {
return days[month];
}
}
/* Beep for a button press*/
static inline void _beep_button()
{
// Play a beep as confirmation for a button press (if applicable)
if (!movement_button_should_sound())
return;
watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
}
/* Beep for entering settings */
static inline void _beep_enable()
{
if (!movement_button_should_sound())
return;
watch_buzzer_play_note(BUZZER_NOTE_G7, 50);
watch_buzzer_play_note(BUZZER_NOTE_REST, 75);
watch_buzzer_play_note(BUZZER_NOTE_C8, 75);
}
/* Beep for leaving settings */
static inline void _beep_disable()
{
if (!movement_button_should_sound())
return;
watch_buzzer_play_note(BUZZER_NOTE_C8, 50);
watch_buzzer_play_note(BUZZER_NOTE_REST, 75);
watch_buzzer_play_note(BUZZER_NOTE_G7, 75);
}
/* Change tick frequency */
static inline void _change_tick_freq(uint8_t freq, deadline_state_t *state)
{
if (state->tick_freq != freq) {
movement_request_tick_frequency(freq);
state->tick_freq = freq;
}
}
/* Determine index of closest deadline */
static uint8_t _closest_deadline(deadline_state_t *state)
{
watch_date_time_t now = watch_rtc_get_date_time();
uint32_t now_ts = watch_utility_date_time_to_unix_time(now, movement_get_current_timezone_offset());
uint32_t min_ts = UINT32_MAX;
uint8_t min_index = 0;
for (uint8_t i = 0; i < DEADLINE_FACE_DATES; i++) {
if (state->deadlines[i] < now_ts || state->deadlines[i] > min_ts)
continue;
min_ts = state->deadlines[i];
min_index = i;
}
return min_index;
}
/* Play background alarm */
static void _background_alarm_play(deadline_state_t *state)
{
/* Use the default alarm from movement and move to foreground */
if (state->alarm_enabled) {
movement_play_alarm();
movement_move_to_face(state->face_idx);
}
}
/* Schedule background alarm */
static void _background_alarm_schedule(deadline_state_t *state)
{
/* We simply re-use the scheduling in the background task */
deadline_face_advise(state);
}
/* Cancel background alarm */
static void _background_alarm_cancel(deadline_state_t *state)
{
movement_cancel_background_task_for_face(state->face_idx);
}
/* Reset deadline to tomorrow */
static inline void _reset_deadline(deadline_state_t *state)
{
/* Get current time and reset hours/minutes/seconds */
watch_date_time_t date_time = watch_rtc_get_date_time();
date_time.unit.second = 0;
date_time.unit.minute = 0;
date_time.unit.hour = 0;
/* Add 24 hours to obtain first second of tomorrow */
uint32_t ts = watch_utility_date_time_to_unix_time(date_time, movement_get_current_timezone_offset());
ts += 24 * 60 * 60;
state->deadlines[state->current_index] = ts;
}
/* Increment date in settings mode. Function taken from `set_time_face.c` */
static void _increment_date(deadline_state_t *state, watch_date_time_t date_time)
{
const uint8_t days_in_month[12] = { 31, 28, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31 };
switch (state->current_page) {
case 0:
/* Only 10 years covered. Fix this sometime next decade */
date_time.unit.year = ((date_time.unit.year % 10) + 1);
break;
case 1:
date_time.unit.month = (date_time.unit.month % 12) + 1;
break;
case 2:
date_time.unit.day = (date_time.unit.day % watch_utility_days_in_month(date_time.unit.month, date_time.unit.year + WATCH_RTC_REFERENCE_YEAR)) + 1;
break;
case 3:
date_time.unit.hour = (date_time.unit.hour + 1) % 24;
break;
case 4:
date_time.unit.minute = (date_time.unit.minute + 1) % 60;
break;
}
uint32_t ts = watch_utility_date_time_to_unix_time(date_time, movement_get_current_timezone_offset());
state->deadlines[state->current_index] = ts;
}
/* Update display in running mode */
static void _running_display(movement_event_t event, deadline_state_t *state)
{
(void) event;
/* Seconds, minutes, hours, days, months, years */
int16_t unit[] = { 0, 0, 0, 0, 0, 0 };
uint8_t i, range[] = { 60, 60, 24, 30, 12, 0 };
char buf[16];
/* Display indicators */
if (state->alarm_enabled)
watch_set_indicator(WATCH_INDICATOR_BELL);
else
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_date_time_t now = watch_rtc_get_date_time();
uint32_t now_ts = watch_utility_date_time_to_unix_time(now, movement_get_current_timezone_offset());
/* Deadline expired */
if (state->deadlines[state->current_index] < now_ts) {
if (state->deadlines[state->current_index] + 24 * 60 * 60 > now_ts)
sprintf(buf, "DL%2dOVER ", state->current_index + 1);
else
sprintf(buf, "DL%2d---- ", state->current_index + 1);
//watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_display_string(buf, 0);
return;
}
/* Get date time structs */
watch_date_time_t deadline = watch_utility_date_time_from_unix_time(state->deadlines[state->current_index], movement_get_current_timezone_offset()
);
/* Calculate naive difference of dates */
unit[0] = deadline.unit.second - now.unit.second;
unit[1] = deadline.unit.minute - now.unit.minute;
unit[2] = deadline.unit.hour - now.unit.hour;
unit[3] = deadline.unit.day - now.unit.day;
unit[4] = deadline.unit.month - now.unit.month;
unit[5] = deadline.unit.year - now.unit.year;
/* Correct errors of naive difference */
for (i = 0; i < 6; i++) {
if (unit[i] < 0) {
/* Correct remaining units */
if (i == 3)
unit[i] += _days_in_month(deadline.unit.month - 1, deadline.unit.year);
else
unit[i] += range[i];
/* Carry over change to next unit if non-zero */
if (i < 5 && unit[i + 1] != 0)
unit[i + 1] -= 1;
}
}
/* Set range */
i = state->current_index + 1;
if (unit[5] > 0) {
/* years:months */
sprintf(buf, "DL%2d%02d%02dYR", i, unit[5] % 100, unit[4] % 12);
} else if (unit[4] > 0) {
/* months:days */
sprintf(buf, "DL%2d%02d%02dMO", i, (unit[5] * 12 + unit[4]) % 100, unit[3] % 32);
} else if (unit[3] > 0) {
/* days:hours */
sprintf(buf, "DL%2d%02d%02ddY", i, unit[3] % 32, unit[2] % 24);
} else {
/* hours:minutes:seconds */
sprintf(buf, "DL%2d%02d%02d%02d", i, unit[2] % 24, unit[1] % 60, unit[0] % 60);
}
watch_display_string(buf, 0);
}
/* Init running mode */
static void _running_init(deadline_state_t *state)
{
(void) state;
watch_clear_indicator(WATCH_INDICATOR_24H);
watch_clear_indicator(WATCH_INDICATOR_PM);
watch_set_colon();
/* Ensure 1Hz updates only */
_change_tick_freq(1, state);
}
/* Loop of running mode */
static bool _running_loop(movement_event_t event, void *context)
{
deadline_state_t *state = (deadline_state_t *) context;
if (event.event_type != EVENT_BACKGROUND_TASK)
_running_display(event, state);
switch (event.event_type) {
case EVENT_ALARM_BUTTON_UP:
_beep_button();
state->current_index = (state->current_index + 1) % DEADLINE_FACE_DATES;
_running_display(event, state);
break;
case EVENT_ALARM_LONG_PRESS:
_beep_enable();
_setting_init(state);
state->mode = DEADLINE_FACE_SETTING;
break;
case EVENT_MODE_BUTTON_UP:
movement_move_to_next_face();
return false;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_LONG_PRESS:
_beep_button();
state->alarm_enabled = !state->alarm_enabled;
if (state->alarm_enabled) {
_background_alarm_schedule(settings, context);
} else {
_background_alarm_cancel(settings, context);
}
_running_display(event, state);
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
case EVENT_BACKGROUND_TASK:
_background_alarm_play(state);
break;
case EVENT_LOW_ENERGY_UPDATE:
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
/* Update display in settings mode */
static void _setting_display(movement_event_t event, deadline_state_t *state, watch_date_time_t date_time)
{
char buf[11];
int i = state->current_index + 1;
if (state->current_page > 2) {
watch_set_colon();
if (movement_clock_mode_24h()) {
watch_set_indicator(WATCH_INDICATOR_24H);
sprintf(buf, "%s%2d%2d%02d ", settings_titles[state->current_page], i, date_time.unit.hour, date_time.unit.minute);
} else {
sprintf(buf, "%s%2d%2d%02d ", settings_titles[state->current_page], i, (date_time.unit.hour % 12) ? (date_time.unit.hour % 12) : 12,
date_time.unit.minute);
if (date_time.unit.hour < 12)
watch_clear_indicator(WATCH_INDICATOR_PM);
else
watch_set_indicator(WATCH_INDICATOR_PM);
}
} else {
watch_clear_colon();
watch_clear_indicator(WATCH_INDICATOR_24H);
watch_clear_indicator(WATCH_INDICATOR_PM);
sprintf(buf, "%s%2d%2d%02d%02d", settings_titles[state->current_page], i, date_time.unit.year + 20, date_time.unit.month, date_time.unit.day);
}
/* Blink up the parameter we are setting */
if (event.subsecond % 2) {
switch (state->current_page) {
case 0:
case 3:
buf[4] = buf[5] = ' ';
break;
case 1:
case 4:
buf[6] = buf[7] = ' ';
break;
case 2:
buf[8] = buf[9] = ' ';
break;
}
}
watch_display_string(buf, 0);
}
/* Init setting mode */
static void _setting_init(deadline_state_t *state)
{
state->current_page = 0;
/* Init fresh deadline to next day */
if (state->deadlines[state->current_index] == 0) {
_reset_deadline(state);
}
/* Ensure 1Hz updates only */
_change_tick_freq(1, state);
}
/* Loop of setting mode */
static bool _setting_loop(movement_event_t event, void *context)
{
deadline_state_t *state = (deadline_state_t *) context;
watch_date_time_t date_time;
date_time = watch_utility_date_time_from_unix_time(state->deadlines[state->current_index], movement_get_current_timezone_offset());
if (event.event_type != EVENT_BACKGROUND_TASK)
_setting_display(event, state, date_time);
switch (event.event_type) {
case EVENT_TICK:
if (state->tick_freq == 8) {
if (HAL_GPIO_BTN_ALARM_read()) {
_increment_date(state, date_time);
_setting_display(event, state, date_time);
} else {
_change_tick_freq(4, state);
}
}
break;
case EVENT_ALARM_LONG_PRESS:
_change_tick_freq(8, state);
break;
case EVENT_ALARM_LONG_UP:
_change_tick_freq(4, state);
break;
case EVENT_LIGHT_LONG_PRESS:
_beep_button();
_reset_deadline(state);
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_BUTTON_UP:
state->current_page = (state->current_page + 1) % SETTINGS_NUM;
_setting_display(event, state, date_time);
break;
case EVENT_ALARM_BUTTON_UP:
_change_tick_freq(4, state);
_increment_date(state, date_time);
_setting_display(event, state, date_time);
break;
case EVENT_TIMEOUT:
_beep_button();
_background_alarm_schedule(settings, context);
_change_tick_freq(1, state);
state->mode = DEADLINE_FACE_RUNNING;
movement_move_to_face(0);
break;
case EVENT_MODE_BUTTON_UP:
_beep_disable();
_background_alarm_schedule(settings, context);
_running_init(state);
_running_display(event, state);
state->mode = DEADLINE_FACE_RUNNING;
break;
case EVENT_BACKGROUND_TASK:
_background_alarm_play(state);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
/* Setup face */
void deadline_face_setup(uint8_t watch_face_index, void **context_ptr)
{
(void) watch_face_index;
if (*context_ptr != NULL)
return; /* Skip setup if context available */
/* Allocate state */
*context_ptr = malloc(sizeof(deadline_state_t));
memset(*context_ptr, 0, sizeof(deadline_state_t));
/* Store face index for background tasks */
deadline_state_t *state = (deadline_state_t *) *context_ptr;
state->face_idx = watch_face_index;
}
/* Activate face */
void deadline_face_activate(void *context)
{
deadline_state_t *state = (deadline_state_t *) context;
/* Set display options */
_running_init(state);
state->mode = DEADLINE_FACE_RUNNING;
state->current_index = _closest_deadline(state);
}
/* Loop face */
bool deadline_face_loop(movement_event_t event, void *context)
{
deadline_state_t *state = (deadline_state_t *) context;
switch (state->mode) {
case DEADLINE_FACE_SETTING:
_setting_loop(event, settings, context);
break;
default:
case DEADLINE_FACE_RUNNING:
_running_loop(event, settings, context);
break;
}
return true;
}
/* Resign face */
void deadline_face_resign(void *context)
{
(void) context;
}
/* Want background task */
movement_watch_face_advisory_t deadline_face_advise(void *context)
{
deadline_state_t *state = (deadline_state_t *) context;
if (!state->alarm_enabled)
return false;
/* Determine closest deadline */
watch_date_time_t now = watch_rtc_get_date_time();
uint32_t now_ts = watch_utility_date_time_to_unix_time(now, movement_get_current_timezone_offset());
uint32_t next_ts = state->deadlines[_closest_deadline(state)];
/* No active deadline */
if (next_ts < now_ts)
return false;
/* No deadline within next 60 seconds */
if (next_ts >= now_ts + 60)
return false;
/* Deadline within next minute. Let's set up an alarm */
watch_date_time_t next = watch_utility_date_time_from_unix_time(next_ts, movement_get_current_timezone_offset());
movement_request_wake();
movement_schedule_background_task_for_face(state->face_idx, next);
return false;
}

View File

@@ -0,0 +1,65 @@
/*
* MIT License
*
* Copyright (c) 2023-2024 Konrad Rieck
*
* 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 DEADLINE_FACE_H_
#define DEADLINE_FACE_H_
#include "movement.h"
/* Modes of face */
typedef enum {
DEADLINE_FACE_RUNNING = 0,
DEADLINE_FACE_SETTING
} deadline_mode_t;
/* Number of deadline dates */
#define DEADLINE_FACE_DATES (4)
/* Deadline configuration */
typedef struct {
deadline_mode_t mode:1;
uint8_t current_page:3;
uint8_t current_index:2;
uint8_t alarm_enabled:1;
uint8_t tick_freq;
uint8_t face_idx;
uint32_t deadlines[DEADLINE_FACE_DATES];
} deadline_state_t;
void deadline_face_setup(uint8_t watch_face_index, void **context_ptr);
void deadline_face_activate(void *context);
bool deadline_face_loop(movement_event_t event, void *context);
void deadline_face_resign(void *context);
movement_watch_face_advisory_t deadline_face_advise(void *context);
#define deadline_face ((const watch_face_t){ \
deadline_face_setup, \
deadline_face_activate, \
deadline_face_loop, \
deadline_face_resign, \
deadline_face_advise \
})
#endif // DEADLINE_FACE_H_

View File

@@ -0,0 +1,322 @@
#include <stdlib.h>
#include <string.h>
#include "discgolf_face.h"
#include "watch.h" // Remember to change number of courses in this file
#include "watch_utility.h"
/*
* Keep track of scores in discgolf or golf!
* The watch face operates in three different modes:
*
* - dg_setting: Select a course
* Enter this mode by holding down the light button. The screen will display
* the label for the hole and the lowest score since last boot.
* Press alarm to loop through the holes. Press the light button to make a
* selection. This will reset all scores and start a new game in dg_idle mode.
*
* -dg_idle: We're playing a hole
* This either shows your current score relative to par, or the score for a
* particular hole.
* At the start of a game, press alarm to loop through the holes and leave it
* your starting hole. For optimal experience, play the course linearly after that
* If you're viewing the hole you're supposed to be playing, the watch face will
* display your score relative to par.
* Use the alarm button to view other holes than the one you're playing, in which
* case the input score for that hole will be displayed, in case it needs changing.
* Long press the alarm button to snap back to currently playing hole.
* To input scores for a hole in this mode, press the light button.
*
* -dg_scoring: Input score for a hole
* In this mode, if the score is 0 (hasn't been entered during this round),
* it will blink, indicating we're in scoring mode. Press the alarm button
* to increment the score up until 15, in which case it loops back to 0.
* Press the light button to save the score for that hole, advance one hole
* if you're not editing an already input score, and returning to idle mode.
*
* When all scores have been entered, the LAP indicator turns on. At that point, if we enter
* dg_setting to select a course, the score for that round is evaluated against the current
* lowest score for that course, and saved if it is better.
*/
///////////////////////////////////////////////////////////////////////////////////////////////
// Enter course data
/* Initialize lowest scores with a high number */
int8_t best[courses];
static const uint8_t pars[][18] = {
{ 3, 3, 4, 3, 3, 3, 5, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 }, // Grafarholt
{ 3, 4, 3, 3, 4, 3, 3, 3, 3, 4, 3, 3, 3, 3, 3, 3, 3, 3 }, // Gufunes
{ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0 }, // Vífilsstaðir
{ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0 }, // Dalvegur
{ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0 }, // Laugardalur
{ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0 }, // Guðmundarlundur
{ 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Víðistaðatún
{ 3, 3, 3, 4, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Fossvogur
{ 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Klambratún
{ 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Seljahverfi
{ 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0 } // Fella- og Hóla
};
static const uint8_t holes[] = { // Number of holes on each course
18,
18,
10,
10,
10,
10,
9,
9,
9,
9,
9
};
/* Two-letter descriptive labels, second field can only be A, B, C, D, E, F, H, I, J, L, N, O, R, T, U and X. */
static const char labels[][2] = {
{ 'G', 'H' },
{ 'G', 'N' },
{ 'V', 'I' },
{ 'D', 'V' },
{ 'L', 'A' },
{ 'G', 'L' },
{ 'V', 'T' },
{ 'F', 'V' },
{ 'K', 'T' },
{ 'S', 'E' },
{ 'F', 'H' }
};
// End of course data
///////////////////////////////////////////////////////////////////////////////////////////////
/* Beep function */
static inline void beep() {
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
}
/* Prep for a new round */
static inline void reset(discgolf_state_t *state) {
for (int i = 0; i < holes[state->course]; i++) {
state->scores[i] = 0;
}
state->hole = 1;
watch_clear_indicator(WATCH_INDICATOR_LAP);
return;
}
/* Total number of throws so far */
static inline uint8_t score_sum(discgolf_state_t *state) {
uint8_t sum = 0;
for (int i = 0; i < holes[state->course]; i++) {
sum = sum + state->scores[i];
}
return sum;
}
/* Count how many holes have been played */
static inline uint8_t count_played(discgolf_state_t *state) {
uint8_t holes_played = 0;
for (int i = 0; i < holes[state->course]; i++) {
if (state->scores[i] > 0) holes_played++;
}
return holes_played;
}
/* Calculate the current score relative to par */
static inline int8_t calculate_score(discgolf_state_t *state) {
uint8_t par_sum = 0;
uint8_t score_sum = 0;
for (int i = 0; i < holes[state->course]; i++) {
if (state->scores[i] > 0) {
par_sum = par_sum + pars[state->course][i];
score_sum = score_sum + state->scores[i];
}
}
return score_sum - par_sum;
}
/* Store score if it's the best so far */
static inline void store_best(discgolf_state_t *state) {
uint8_t played = count_played(state);
if ( played == holes[state->course] ) {
int8_t high_score = calculate_score(state);
if (high_score < best[state->course] ) {
best[state->course] = high_score;
}
}
}
/* Configuration at boot, the high score array can be initialized with your high scores if they're known */
void discgolf_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(discgolf_state_t));
discgolf_state_t *state = (discgolf_state_t *)*context_ptr;
memset(*context_ptr, 0, sizeof(discgolf_state_t));
state->hole = 1;
state->course = 0;
state->playing = holes[state->course] + 1;
for (int i = 0; i < courses; i++) best[i] = 99;
state->mode = dg_setting;
}
}
void discgolf_face_activate(void *context) {
watch_clear_colon();
discgolf_state_t *state = (discgolf_state_t *)context;
/* If we were playing, go to current hole */
if (state->playing <= holes[0]) {
state->hole = state->playing;
}
/* Set LAP if round finished */
if (count_played(state) == holes[state->course] ) {
watch_set_indicator(WATCH_INDICATOR_LAP);
}
movement_request_tick_frequency(4);
}
bool discgolf_face_loop(movement_event_t event, void *context) {
discgolf_state_t *state = (discgolf_state_t *)context;
switch (event.event_type) {
case EVENT_TIMEOUT:
/* Snap to first screen if we're not playing*/
if ( count_played(state) == holes[state->course] || state->mode == dg_setting) {
movement_move_to_face(0);
}
/* Cancel scoring if timed out */
if (state->mode == dg_scoring) {
state->scores[state->hole] = 0;
state->mode = dg_idle;
}
break;
/* Advance if not scoring */
case EVENT_MODE_BUTTON_UP:
if ( state->mode != dg_scoring ) {
movement_move_to_next_face();
}
break;
/* Go to default face if not scoring */
case EVENT_MODE_LONG_PRESS:
if ( state->mode != dg_scoring ) {
movement_move_to_face(0);
}
break;
case EVENT_LIGHT_BUTTON_UP:
switch ( state->mode ) {
case dg_idle:
/* Check if selected hole is the first one */
if ( score_sum(state) == 0 ) {
state->playing = state->hole;
}
/* Enter scoring mode */
state->mode = dg_scoring;
break;
case dg_scoring:
/* Set the LAP indicator if all scores are entered */
if (count_played(state) == holes[state->course] ) {
watch_set_indicator(WATCH_INDICATOR_LAP);
}
/* Advance to next hole if not editing previously set score */
if ( state->hole == state->playing ) {
if (state->hole < holes[state->course]) state->hole++;
else state->hole = 1;
if (state->playing < holes[state->course]) state->playing++;
else state->playing = 1;
}
/* Return to idle */
state->mode = dg_idle;
break;
case dg_setting:
/* Return to idle */
state->playing = holes[state->course] + 1;
state->mode = dg_idle;
break;
}
beep();
break;
case EVENT_ALARM_BUTTON_UP:
switch (state->mode) {
/* Setting, loop through courses */
case dg_setting:
state->course = (state->course + 1) % courses;
break;
/* Scoring, increment score for current hole */
case dg_scoring:
state->scores[state->hole - 1] = (state->scores[state->hole - 1] + 1) % 16; // Loop around at 15
break;
/* Idle, loop through holes */
case dg_idle:
if (state->hole < holes[state->course]) {
state->hole++;
} else { state->hole = 1; }
break;
}
break;
case EVENT_LIGHT_LONG_PRESS:
/* Enter setting mode, reset state */
if ( state->mode == dg_idle ) {
state->mode = dg_setting;
store_best(state);
reset(state);
beep();
}
break;
case EVENT_ALARM_LONG_PRESS:
/* Snap back to currently playing hole if we've established one*/
if ( (state->mode == dg_idle) && (state->hole != state->playing) && (state->playing <= holes[state->course]) ) {
state->hole = state->playing;
beep();
}
break;
default:
break;
}
char buf[21];
char prefix;
int8_t diff;
switch (state->mode) {
/* Setting mode, display course label and high score */
case dg_setting:
if ( best[state->course] < 0 ) {
prefix = '-';
} else { prefix = ' '; }
sprintf(buf, "%c%c %c%2d ", labels[state->course][0], labels[state->course][1], prefix, abs(best[state->course]));
break;
/* Idle, show relative or input score */
case dg_idle:
if (state->hole == state->playing) {
diff = calculate_score(state);
if ( diff < 0 ) {
prefix = '-';
} else { prefix = ' '; }
sprintf(buf, "%c%c%2d %c%2d ", labels[state->course][0], labels[state->course][1], state->hole, prefix, abs(diff));
} else {
sprintf(buf, "%c%c%2d %2d ", labels[state->course][0], labels[state->course][1], state->hole, state->scores[state->hole - 1]);
}
break;
/* Scoring, show set score */
case dg_scoring:
sprintf(buf, "%c%c%2d %2d ", labels[state->course][0], labels[state->course][1], state->hole, state->scores[state->hole - 1]);
break;
}
/* Blink during scoring */
if (event.subsecond % 2 && state->mode == dg_scoring) {
buf[6] = buf[7] = ' ';
}
/* Draw screen */
watch_display_string(buf, 0);
return true;
}
void discgolf_face_resign(void *context) {
(void) context;
watch_clear_indicator(WATCH_INDICATOR_LAP);
}

View File

@@ -0,0 +1,94 @@
/*
* MIT License
*
* Copyright (c) 2023 Þorsteinn Jón Gautason
*
* 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 DISCGOLF_FACE_H_
#define DISCGOLF_FACE_H_
/*
* DISC GOLF face
*
* Keep track of scores in discgolf or golf!
* The watch face operates in three different modes:
*
* - dg_setting: Select a course
* Enter this mode by holding down the light button. The screen will display
* the label for the hole and the lowest score since last boot.
* Press alarm to loop through the holes. Press the light button to make a
* selection. This will reset all scores and start a new game in dg_idle mode.
*
* -dg_idle: We're playing a hole
* This either shows your current score relative to par, or the score for a
* particular hole.
* At the start of a game, press alarm to loop through the holes and leave it
* your starting hole. For optimal experience, play the course linearly after that
* If you're viewing the hole you're supposed to be playing, the watch face will
* display your score relative to par.
* Use the alarm button to view other holes than the one you're playing, in which
* case the input score for that hole will be displayed, in case it needs changing.
* Long press the alarm button to snap back to currently playing hole.
* To input scores for a hole in this mode, press the light button.
*
* -dg_scoring: Input score for a hole
* In this mode, if the score is 0 (hasn't been entered during this round),
* it will blink, indicating we're in scoring mode. Press the alarm button
* to increment the score up until 15, in which case it loops back to 0.
* Press the light button to save the score for that hole, advance one hole
* if you're not editing an already input score, and returning to idle mode.
*
* When all scores have been entered, the LAP indicator turns on. At that point, if we enter
* dg_setting to select a course, the score for that round is evaluated against the current
* lowest score for that course, and saved if it is better.
*/
#include "movement.h"
#define courses 11
typedef enum {
dg_setting, // We are selecting a course
dg_scoring, // We are inputting our score
dg_idle, // We have input our score and are playing a hole
} discgolf_mode_t;
typedef struct {
uint8_t course; // Index for course selection, from 0
uint8_t hole; // Index for current hole, from 1
uint8_t playing; // Current hole
int scores[18]; // Scores for each played hole
discgolf_mode_t mode; // Watch face mode
} discgolf_state_t;
void discgolf_face_setup(uint8_t watch_face_index, void ** context_ptr);
void discgolf_face_activate(void *context);
bool discgolf_face_loop(movement_event_t event, void *context);
void discgolf_face_resign(void *context);
#define discgolf_face ((const watch_face_t){ \
discgolf_face_setup, \
discgolf_face_activate, \
discgolf_face_loop, \
discgolf_face_resign, \
NULL, \
})
#endif // DISCGOLF_FACE_H_

View File

@@ -0,0 +1,331 @@
/*
* MIT License
*
* Copyright (c) 2023 Tobias Raayoni Last / @randogoth
* Copyright (c) 2022 Andreas Nebinger
*
* 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 "dual_timer_face.h"
#include "watch.h"
#include "watch_utility.h"
#include "watch_rtc.h"
/*
* IMPORTANT: This watch face uses the same TC2 callback counter as the Stock Stopwatch
* watch-face. It works through calling a global handler function. The two watch-faces
* therefore can't coexist within the same firmware. If you want to compile this watch-face
* then you need to remove the line <../watch_faces/complication/fast_stopwatch_face.c \>
* from the Makefile.
*/
// FROM fast_stopwatch_face.c ////////////////////////////////////////////////
// Copyright (c) 2022 Andreas Nebinger
#if __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#else
#include "../../../watch-library/hardware/include/saml22j18a.h"
#include "../../../watch-library/hardware/include/component/tc.h"
#include "../../../watch-library/hardware/hri/hri_tc_l22.h"
#endif
static const watch_date_time_t distant_future = {.unit = {0, 0, 0, 1, 1, 63}};
static bool _is_running;
static uint32_t _ticks;
#if __EMSCRIPTEN__
static long _em_interval_id = 0;
void em_dual_timer_cb_handler(void *userData) {
// interrupt handler for emscripten 128 Hz callbacks
(void) userData;
_ticks++;
}
static void _dual_timer_cb_initialize() { }
static inline void _dual_timer_cb_stop() {
emscripten_clear_interval(_em_interval_id);
_em_interval_id = 0;
_is_running = false;
}
static inline void _dual_timer_cb_start() {
// initiate 128 hz callback
_em_interval_id = emscripten_set_interval(em_dual_timer_cb_handler, (double)(1000/128), (void *)NULL);
}
#else
static inline void _dual_timer_cb_start() {
// start the TC2 timer
hri_tc_set_CTRLA_ENABLE_bit(TC2);
_is_running = true;
}
static inline void _dual_timer_cb_stop() {
// stop the TC2 timer
hri_tc_clear_CTRLA_ENABLE_bit(TC2);
_is_running = false;
}
static void _dual_timer_cb_initialize() {
// setup and initialize TC2 for a 64 Hz interrupt
hri_mclk_set_APBCMASK_TC2_bit(MCLK);
hri_gclk_write_PCHCTRL_reg(GCLK, TC2_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK3 | GCLK_PCHCTRL_CHEN);
_dual_timer_cb_stop();
hri_tc_write_CTRLA_reg(TC2, TC_CTRLA_SWRST);
hri_tc_wait_for_sync(TC2, TC_SYNCBUSY_SWRST);
hri_tc_write_CTRLA_reg(TC2, TC_CTRLA_PRESCALER_DIV64 | // 32 Khz divided by 64 divided by 4 results in a 128 Hz interrupt
TC_CTRLA_MODE_COUNT8 |
TC_CTRLA_RUNSTDBY);
hri_tccount8_write_PER_reg(TC2, 3);
hri_tc_set_INTEN_OVF_bit(TC2);
NVIC_ClearPendingIRQ(TC2_IRQn);
NVIC_EnableIRQ (TC2_IRQn);
}
// you need to take fast_stopwatch.c out of the Makefile or this will create a conflict
// you have to choose between one of the stopwatches
void TC2_Handler(void) {
// interrupt handler for TC2 (globally!)
_ticks++;
TC2->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
}
#endif
// STATIC FUNCTIONS ///////////////////////////////////////////////////////////
/** @brief converts tick counts to duration struct for time display
*/
static dual_timer_duration_t ticks_to_duration(uint32_t ticks) {
dual_timer_duration_t duration;
uint8_t hours = 0;
uint8_t days = 0;
// count hours and days
while (ticks >= (128 * 60 * 60)) {
ticks -= (128 * 60 * 60);
hours++;
if (hours >= 24) {
hours -= 24;
days++;
}
}
// convert minutes, seconds, centiseconds
duration.centiseconds = (ticks & 0x7F) * 100 / 128;
duration.seconds = (ticks >> 7) % 60;
duration.minutes = (ticks >> 7) / 60;
duration.hours = hours;
duration.days = days;
return duration;
}
/** @brief starts one of the dual timers
* @details Starts a dual timer. If no previous timer is running it starts the global
* tick counter. If a previous timer is already running it registers the current tick.
*/
static void start_timer(dual_timer_state_t *state, bool timer) {
// if it is not running yet, run it
if ( !_is_running ) {
_is_running = true;
movement_request_tick_frequency(16);
state->start_ticks[timer] = 0;
state->stop_ticks[timer] = 0;
_ticks = 0;
_dual_timer_cb_start();
movement_schedule_background_task(distant_future);
} else {
// if another timer is already running save the current tick
state->start_ticks[timer] = _ticks;
state->stop_ticks[timer] = _ticks;
}
state->running[timer] = true;
}
/** @brief stops one of the dual timers
* @details Stops a dual timer. If no other timer is running it stops the global
* tick counter. If another timer is already running it registers the current stop tick.
*/
static void stop_timer(dual_timer_state_t *state, bool timer) {
// stop timer and save duration
state->stop_ticks[timer] = _ticks;
state->duration[timer] = ticks_to_duration(state->stop_ticks[timer] - state->start_ticks[timer]);
state->running[timer] = false;
// if the other timer is not running, stop callback
if ( state->running[!timer] == false ) {
_is_running = false;
_dual_timer_cb_stop();
movement_request_tick_frequency(1);
movement_cancel_background_task();
}
}
/** @brief displays the measured time for each of the dual timers
* @details displays the dual timer. Below 1 hour it displays the timed minutes, seconds,
* and centiseconds. Above that it shows the timed hours, minutes, and seconds. If it
* has run for more than a day it shows the days, hours, and minutes.
* When the timer is running, the colon blinks every half second.
* It also indicates at the top if another counter is running and for how long.
*/
static void dual_timer_display(dual_timer_state_t *state) {
char buf[11];
char oi[3];
// get the current time count of the selected counter
dual_timer_duration_t timer = state->running[state->show] ? ticks_to_duration(state->stop_ticks[state->show] - state->start_ticks[state->show]) : state->duration[state->show];
// get the current time count of the other counter
dual_timer_duration_t other = ticks_to_duration(state->stop_ticks[!state->show] - state->start_ticks[!state->show]);
if ( timer.days > 0 )
sprintf(buf, "%02u%02u%02u", timer.days, timer.hours, timer.minutes);
else if ( timer.hours > 0 )
sprintf(buf, "%02u%02u%02u", timer.hours, timer.minutes, timer.seconds);
else
sprintf(buf, "%02u%02u%02u", timer.minutes, timer.seconds, timer.centiseconds);
watch_display_string(buf, 4);
// which counter is displayed
watch_display_string(state->show ? "B" : "A", 0);
// indicate whether other counter is running
watch_display_string(state->running[!state->show] && (_ticks % 100) < 50 ? "+" : " ", 1);
// indicate for how long the other counter has been running
sprintf(oi, "%2u", other.days > 0 ? other.days : (other.hours > 0 ? other.hours : (other.minutes > 0 ? other.minutes : (other.seconds > 0 ? other.seconds : other.centiseconds))));
watch_display_string( (state->stop_ticks[!state->show] - state->start_ticks[!state->show]) > 0 ? oi : " ", 2);
// blink colon when running
if ( timer.centiseconds > 50 || !state->running[state->show] ) watch_set_colon();
else watch_clear_colon();
}
// PUBLIC WATCH FACE FUNCTIONS ////////////////////////////////////////////////
void dual_timer_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(dual_timer_state_t));
memset(*context_ptr, 0, sizeof(dual_timer_state_t));
_ticks = 0;
}
if (!_is_running) {
_dual_timer_cb_initialize();
}
}
void dual_timer_face_activate(void *context) {
(void) context;
if (_is_running) {
movement_schedule_background_task(distant_future);
}
}
bool dual_timer_face_loop(movement_event_t event, void *context) {
dual_timer_state_t *state = (dual_timer_state_t *)context;
// timers stop at 99:23:59:59:99
if ( (_ticks - state->start_ticks[0]) >= 1105919999 )
stop_timer(state, 0);
if ( (_ticks - state->start_ticks[1]) >= 1105919999 )
stop_timer(state, 1);
switch (event.event_type) {
case EVENT_ACTIVATE:
watch_set_colon();
if (_is_running) {
movement_request_tick_frequency(16);
if ( state->running[0] )
state->show = 0;
else state->show = 1;
} else {
if (state->stop_ticks[0] > 0 || state->stop_ticks[1] > 0)
dual_timer_display(state);
else watch_display_string("A 000000", 0);
}
break;
case EVENT_TICK:
if ( _is_running ) {
// update stop ticks
if ( state->running[0] )
state->stop_ticks[0] = _ticks;
if ( state->running[1] )
state->stop_ticks[1] = _ticks;
dual_timer_display(state);
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
// start/stop timer B
state->running[1] = !state->running[1];
if ( state->running[1] ) {
start_timer(state, 1);
} else {
stop_timer(state, 1);
}
break;
case EVENT_ALARM_BUTTON_DOWN:
// start/stop timer A
state->running[0] = !state->running[0];
if ( state->running[0] ) {
start_timer(state, 0);
} else {
stop_timer(state, 0);
}
break;
case EVENT_MODE_BUTTON_DOWN:
// switch between the timers
state->show = !state->show;
dual_timer_display(state);
break;
case EVENT_MODE_BUTTON_UP:
// don't switch to next face...
break;
case EVENT_MODE_LONG_PRESS:
// ...but do it on long press MODE!
movement_move_to_next_face();
break;
case EVENT_TIMEOUT:
// go back to
if (!_is_running) movement_move_to_face(0);
break;
case EVENT_LOW_ENERGY_UPDATE:
dual_timer_display(state);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void dual_timer_face_resign(void *context) {
(void) context;
movement_cancel_background_task();
// handle any cleanup before your watch face goes off-screen.
}

View File

@@ -0,0 +1,108 @@
/*
* MIT License
*
* Copyright (c) 2023 Tobias Raayoni Last / @randogoth
* Copyright (c) 2022 Andreas Nebinger
*
* 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 DUAL_TIMER_FACE_H_
#define DUAL_TIMER_FACE_H_
/*
* DUAL TIMER
* ==========
*
* Inspired by special ops and tactical trope targeted watches like the Nixon Regulus
* that feature two chronographs for timing two simultaneous events, here is a watch
* face that expands upon Andreas Nebinger's Stock Stopwatch Face code to implement this
* functionality.
*
* ALARM starts/stops timer A, resets on the next start
* LIGHT starts/stops timer B, resets on the next start
*
* When a timer is running, tapping MODE toggles between displaying timers A or B, as
* indicated at the top of the display.
*
* The currently selected timer shows minutes, seconds, and 100ths of seconds until the
* timed event passes the hour mark. Then it shows hours, minutes, and seconds. Once
* it runs for more than a day it shows days, hours, and minutes. The blinking colon
* indicates that the timer is running.
*
* The longest time span the timers are able to track as 99 days and 23:59:59:99 hours and
* they will simply stop.
*
* If the other timer that is not currently selected is also running then a plus sign is
* blinking next to the A or B indicator. Its progress is indicated by showing the timer's
* currently highest time unit ( 100ths of seconds if less than a second, seconds if less
* than a minute, minutes if less than an hour, hours if less than a day, days if more than
* a day).
*
* Please Note: If at least one timer is running then the default function of the MODE
* button to move to the next watch face is disabled to be able to use it to toggle between
* the timers. In this case LONG PRESSING MODE will move to the next face instead of moving
* back to the default watch face.
*
* IMPORTANT: This watch face uses the same TC2 callback counter as the Stock Stopwatch
* watch-face. It works through calling a global handler function. The two watch-faces
* therefore can't coexist within the same firmware. If you want to compile this watch-face
* then you need to remove the line <../watch_faces/complication/fast_stopwatch_face.c \>
* from the Makefile.
*/
#include "movement.h"
typedef struct {
uint8_t centiseconds : 7; // 0-59
uint8_t seconds : 6; // 0-59
uint8_t minutes : 6; // 0-59
uint8_t hours : 5; // 0-23
uint8_t days : 7; // 0-99
} dual_timer_duration_t;
typedef struct {
uint32_t start_ticks[2];
uint32_t stop_ticks[2];
dual_timer_duration_t duration[2];
bool running[2];
bool show;
} dual_timer_state_t;
void dual_timer_face_setup(uint8_t watch_face_index, void ** context_ptr);
void dual_timer_face_activate(void *context);
bool dual_timer_face_loop(movement_event_t event, void *context);
void dual_timer_face_resign(void *context);
#if __EMSCRIPTEN__
void em_dual_timer_cb_handler(void *userData);
#else
void TC2_Handler(void);
#endif
#define dual_timer_face ((const watch_face_t){ \
dual_timer_face_setup, \
dual_timer_face_activate, \
dual_timer_face_loop, \
dual_timer_face_resign, \
NULL, \
})
#endif // DUAL_TIMER_FACE_H_

View File

@@ -0,0 +1,614 @@
/*
* MIT License
*
* Copyright (c) 2024 <David Volovskiy>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include <string.h>
#include "endless_runner_face.h"
typedef enum {
JUMPING_FINAL_FRAME = 0,
NOT_JUMPING,
JUMPING_START,
} RunnerJumpState;
typedef enum {
SCREEN_TITLE = 0,
SCREEN_PLAYING,
SCREEN_LOSE,
SCREEN_TIME,
SCREEN_COUNT
} RunnerCurrScreen;
typedef enum {
DIFF_BABY = 0, // FREQ_SLOW FPS; MIN_ZEROES 0's min; Jump is JUMP_FRAMES_EASY frames
DIFF_EASY, // FREQ FPS; MIN_ZEROES 0's min; Jump is JUMP_FRAMES_EASY frames
DIFF_NORM, // FREQ FPS; MIN_ZEROES 0's min; Jump is JUMP_FRAMES frames
DIFF_HARD, // FREQ FPS; MIN_ZEROES_HARD 0's min; jump is JUMP_FRAMES frames
DIFF_FUEL, // Mode where the top-right displays the amoount of fuel that you can be above the ground for, dodging obstacles. When on the ground, your fuel recharges.
DIFF_FUEL_1, // Same as DIFF_FUEL, but if your fuel is 0, then you won't recharge
DIFF_COUNT
} RunnerDifficulty;
#define NUM_GRID 12 // This the length that the obstacle track can be on
#define FREQ 8 // Frequency request for the game
#define FREQ_SLOW 4 // Frequency request for baby mode
#define JUMP_FRAMES 2 // Wait this many frames on difficulties above EASY before coming down from the jump button pressed
#define JUMP_FRAMES_EASY 3 // Wait this many frames on difficulties at or below EASY before coming down from the jump button pressed
#define MIN_ZEROES 4 // At minimum, we'll have this many spaces between obstacles
#define MIN_ZEROES_HARD 3 // At minimum, we'll have this many spaces between obstacles on hard mode
#define MAX_HI_SCORE 9999 // Max hi score to store and display on the title screen.
#define MAX_DISP_SCORE 39 // The top-right digits can't properly display above 39
#define JUMP_FRAMES_FUEL 30 // The max fuel that fuel that the fuel mode game will hold
#define JUMP_FRAMES_FUEL_RECHARGE 3 // How much fuel each frame on the ground adds
#define MAX_DISP_SCORE_FUEL 9 // Since the fuel mode displays the score in the weekday slot, two digits will display wonky data
typedef struct {
uint32_t obst_pattern;
uint16_t obst_indx : 8;
uint16_t jump_state : 5;
uint16_t sec_before_moves : 3;
uint16_t curr_score : 10;
uint16_t curr_screen : 4;
bool loc_2_on;
bool loc_3_on;
bool success_jump;
bool fuel_mode;
uint8_t fuel;
} game_state_t;
static game_state_t game_state;
static const uint8_t _num_bits_obst_pattern = sizeof(game_state.obst_pattern) * 8;
static void print_binary(uint32_t value, int bits) {
#if __EMSCRIPTEN__
for (int i = bits - 1; i >= 0; i--) {
// Print each bit
printf("%lu", (value >> i) & 1);
// Optional: add a space every 4 bits for readability
if (i % 4 == 0 && i != 0) {
printf(" ");
}
}
printf("\r\n");
#else
(void) value;
(void) bits;
#endif
return;
}
static uint32_t get_random(uint32_t max) {
#if __EMSCRIPTEN__
return rand() % max;
#else
return arc4random_uniform(max);
#endif
}
static uint32_t get_random_nonzero(uint32_t max) {
uint32_t random;
do
{
random = get_random(max);
} while (random == 0);
return random;
}
static uint32_t get_random_kinda_nonzero(uint32_t max) {
// Returns a number that's between 1 and max, unless max is 0 or 1, then it returns 0 to max.
if (max == 0) return 0;
else if (max == 1) return get_random(max);
return get_random_nonzero(max);
}
static uint32_t get_random_fuel(uint32_t prev_val) {
static uint8_t prev_rand_subset = 0;
uint32_t rand;
uint8_t max_ones, subset;
uint32_t rand_legal = 0;
prev_val = prev_val & ~0xFFFF;
for (int i = 0; i < 2; i++) {
subset = 0;
max_ones = 8;
if (prev_rand_subset > 4)
max_ones -= prev_rand_subset;
rand = get_random_kinda_nonzero(max_ones);
if (rand > 5 && prev_rand_subset) rand = 5; // The gap of one or two is awkward
for (uint32_t j = 0; j < rand; j++) {
subset |= (1 << j);
}
if (prev_rand_subset >= 7)
subset = subset << 1;
subset &= 0xFF;
rand_legal |= subset << (8 * i);
prev_rand_subset = rand;
}
rand_legal = prev_val | rand_legal;
print_binary(rand_legal, 32);
return rand_legal;
}
static uint32_t get_random_legal(uint32_t prev_val, uint16_t difficulty) {
/** @brief A legal random number starts with the previous number (which should be the 12 bits on the screen).
* @param prev_val The previous value to tack onto. The return will have its first NUM_GRID MSBs be the same as prev_val, and the rest be new
* @param difficulty To dictate how spread apart the obsticles must be
* @return the new random value, where it's first NUM_GRID MSBs are the same as prev_val
*/
uint8_t min_zeros = (difficulty == DIFF_HARD) ? MIN_ZEROES_HARD : MIN_ZEROES;
uint32_t max = (1 << (_num_bits_obst_pattern - NUM_GRID)) - 1;
uint32_t rand = get_random_nonzero(max);
uint32_t rand_legal = 0;
prev_val = prev_val & ~max;
for (int i = (NUM_GRID + 1); i <= _num_bits_obst_pattern; i++) {
uint32_t mask = 1 << (_num_bits_obst_pattern - i);
bool msb = (rand & mask) >> (_num_bits_obst_pattern - i);
if (msb) {
rand_legal = rand_legal << min_zeros;
i+=min_zeros;
}
rand_legal |= msb;
rand_legal = rand_legal << 1;
}
rand_legal = rand_legal & max;
for (int i = 0; i <= min_zeros; i++) {
if (prev_val & (1 << (i + _num_bits_obst_pattern - NUM_GRID))){
rand_legal = rand_legal >> (min_zeros - i);
break;
}
}
rand_legal = prev_val | rand_legal;
print_binary(rand_legal, 32);
return rand_legal;
}
static void display_ball(bool jumping) {
if (!jumping) {
watch_set_pixel(0, 21);
watch_set_pixel(1, 21);
watch_set_pixel(0, 20);
watch_set_pixel(1, 20);
watch_clear_pixel(1, 17);
watch_clear_pixel(2, 20);
watch_clear_pixel(2, 21);
}
else {
watch_clear_pixel(0, 21);
watch_clear_pixel(1, 21);
watch_clear_pixel(0, 20);
watch_set_pixel(1, 20);
watch_set_pixel(1, 17);
watch_set_pixel(2, 20);
watch_set_pixel(2, 21);
}
}
static void display_score(uint8_t score) {
char buf[3];
if (game_state.fuel_mode) {
score %= (MAX_DISP_SCORE_FUEL + 1);
sprintf(buf, "%1d", score);
watch_display_string(buf, 0);
}
else {
score %= (MAX_DISP_SCORE + 1);
sprintf(buf, "%2d", score);
watch_display_string(buf, 2);
}
}
static void add_to_score(endless_runner_state_t *state) {
if (game_state.curr_score <= MAX_HI_SCORE) {
game_state.curr_score++;
if (game_state.curr_score > state -> hi_score)
state -> hi_score = game_state.curr_score;
}
game_state.success_jump = true;
display_score(game_state.curr_score);
}
static void display_fuel(uint8_t subsecond, uint8_t difficulty) {
char buf[4];
if (difficulty == DIFF_FUEL_1 && game_state.fuel == 0 && subsecond % (FREQ/2) == 0) {
watch_display_string(" ", 2); // Blink the 0 fuel to show it cannot be refilled.
return;
}
sprintf(buf, "%2d", game_state.fuel);
watch_display_string(buf, 2);
}
static void check_and_reset_hi_score(endless_runner_state_t *state) {
// Resets the hi score at the beginning of each month.
watch_date_time_t date_time = watch_rtc_get_date_time();
if ((state -> year_last_hi_score != date_time.unit.year) ||
(state -> month_last_hi_score != date_time.unit.month))
{
// The high score resets itself every new month.
state -> hi_score = 0;
state -> year_last_hi_score = date_time.unit.year;
state -> month_last_hi_score = date_time.unit.month;
}
}
static void display_difficulty(uint16_t difficulty) {
switch (difficulty)
{
case DIFF_BABY:
watch_display_string(" b", 2);
break;
case DIFF_EASY:
watch_display_string(" E", 2);
break;
case DIFF_HARD:
watch_display_string(" H", 2);
break;
case DIFF_FUEL:
watch_display_string(" F", 2);
break;
case DIFF_FUEL_1:
watch_display_string("1F", 2);
break;
case DIFF_NORM:
default:
watch_display_string(" N", 2);
break;
}
game_state.fuel_mode = difficulty >= DIFF_FUEL && difficulty <= DIFF_FUEL_1;
}
static void change_difficulty(endless_runner_state_t *state) {
state -> difficulty = (state -> difficulty + 1) % DIFF_COUNT;
display_difficulty(state -> difficulty);
if (state -> soundOn) {
if (state -> difficulty == 0) watch_buzzer_play_note(BUZZER_NOTE_B4, 30);
else watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
}
}
static void toggle_sound(endless_runner_state_t *state) {
state -> soundOn = !state -> soundOn;
if (state -> soundOn){
watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
watch_set_indicator(WATCH_INDICATOR_BELL);
}
else {
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
}
static void display_title(endless_runner_state_t *state) {
uint16_t hi_score = state -> hi_score;
uint8_t difficulty = state -> difficulty;
bool sound_on = state -> soundOn;
game_state.curr_screen = SCREEN_TITLE;
memset(&game_state, 0, sizeof(game_state));
game_state.sec_before_moves = 1; // The first obstacles will all be 0s, which is about an extra second of delay.
if (sound_on) game_state.sec_before_moves--; // Start chime is about 1 second
watch_set_colon();
if (hi_score > MAX_HI_SCORE) {
watch_display_string("ER HS --", 0);
}
else {
char buf[14];
sprintf(buf, "ER HS%4d", hi_score);
watch_display_string(buf, 0);
}
display_difficulty(difficulty);
}
static void display_time(watch_date_time_t date_time, bool clock_mode_24h) {
static watch_date_time_t previous_date_time;
char buf[6 + 1];
// If the hour needs updating or it's the first time displaying the time
if ((game_state.curr_screen != SCREEN_TIME) || (date_time.unit.hour != previous_date_time.unit.hour)) {
uint8_t hour = date_time.unit.hour;
game_state.curr_screen = SCREEN_TIME;
if (clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
else {
if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM);
hour %= 12;
if (hour == 0) hour = 12;
}
watch_set_colon();
sprintf( buf, "%2d%02d ", hour, date_time.unit.minute);
watch_display_string(buf, 4);
}
// If both digits of the minute need updating
else if ((date_time.unit.minute / 10) != (previous_date_time.unit.minute / 10)) {
sprintf( buf, "%02d ", date_time.unit.minute);
watch_display_string(buf, 6);
}
// If only the ones-place of the minute needs updating.
else if (date_time.unit.minute != previous_date_time.unit.minute) {
sprintf( buf, "%d ", date_time.unit.minute % 10);
watch_display_string(buf, 7);
}
previous_date_time.reg = date_time.reg;
}
static void begin_playing(endless_runner_state_t *state) {
uint8_t difficulty = state -> difficulty;
game_state.curr_screen = SCREEN_PLAYING;
watch_clear_colon();
movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ);
if (game_state.fuel_mode) {
watch_display_string(" ", 0);
game_state.obst_pattern = get_random_fuel(0);
if ((16 * JUMP_FRAMES_FUEL_RECHARGE) < JUMP_FRAMES_FUEL) // 16 frames of zeros at the start of a level
game_state.fuel = JUMP_FRAMES_FUEL - (16 * JUMP_FRAMES_FUEL_RECHARGE); // Have it below its max to show it counting up when starting.
if (game_state.fuel < JUMP_FRAMES_FUEL_RECHARGE) game_state.fuel = JUMP_FRAMES_FUEL_RECHARGE;
}
else {
watch_display_string(" ", 2);
game_state.obst_pattern = get_random_legal(0, difficulty);
}
game_state.jump_state = NOT_JUMPING;
display_ball(game_state.jump_state != NOT_JUMPING);
display_score( game_state.curr_score);
if (state -> soundOn){
watch_buzzer_play_note(BUZZER_NOTE_C5, 200);
watch_buzzer_play_note(BUZZER_NOTE_E5, 200);
watch_buzzer_play_note(BUZZER_NOTE_G5, 200);
}
}
static void display_lose_screen(endless_runner_state_t *state) {
game_state.curr_screen = SCREEN_LOSE;
game_state.curr_score = 0;
watch_display_string(" LOSE ", 0);
if (state -> soundOn)
watch_buzzer_play_note(BUZZER_NOTE_A1, 600);
else
delay_ms(600);
}
static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t *state) {
static bool prev_obst_pos_two = 0;
switch (grid_loc)
{
case 2:
game_state.loc_2_on = obstacle;
if (obstacle)
watch_set_pixel(0, 20);
else if (game_state.jump_state != NOT_JUMPING) {
watch_clear_pixel(0, 20);
if (game_state.fuel_mode && prev_obst_pos_two)
add_to_score(state);
}
prev_obst_pos_two = obstacle;
break;
case 3:
game_state.loc_3_on = obstacle;
if (obstacle)
watch_set_pixel(1, 21);
else if (game_state.jump_state != NOT_JUMPING)
watch_clear_pixel(1, 21);
break;
case 1:
if (!game_state.fuel_mode && obstacle) // If an obstacle is here, it means the ball cleared it
add_to_score(state);
//fall through
case 0:
case 5:
if (obstacle)
watch_set_pixel(0, 18 + grid_loc);
else
watch_clear_pixel(0, 18 + grid_loc);
break;
case 4:
if (obstacle)
watch_set_pixel(1, 22);
else
watch_clear_pixel(1, 22);
break;
case 6:
if (obstacle)
watch_set_pixel(1, 0);
else
watch_clear_pixel(1, 0);
break;
case 7:
case 8:
if (obstacle)
watch_set_pixel(0, grid_loc - 6);
else
watch_clear_pixel(0, grid_loc - 6);
break;
case 9:
case 10:
if (obstacle)
watch_set_pixel(0, grid_loc - 5);
else
watch_clear_pixel(0, grid_loc - 5);
break;
case 11:
if (obstacle)
watch_set_pixel(1, 6);
else
watch_clear_pixel(1, 6);
break;
default:
break;
}
}
static void stop_jumping(endless_runner_state_t *state) {
game_state.jump_state = NOT_JUMPING;
display_ball(game_state.jump_state != NOT_JUMPING);
if (state -> soundOn){
if (game_state.success_jump)
watch_buzzer_play_note(BUZZER_NOTE_C5, 60);
else
watch_buzzer_play_note(BUZZER_NOTE_C3, 60);
}
game_state.success_jump = false;
}
static void display_obstacles(endless_runner_state_t *state) {
for (int i = 0; i < NUM_GRID; i++) {
// Use a bitmask to isolate each bit and shift it to the least significant position
uint32_t mask = 1 << ((_num_bits_obst_pattern - 1) - i);
bool obstacle = (game_state.obst_pattern & mask) >> ((_num_bits_obst_pattern - 1) - i);
display_obstacle(obstacle, i, state);
}
game_state.obst_pattern = game_state.obst_pattern << 1;
game_state.obst_indx++;
if (game_state.fuel_mode) {
if (game_state.obst_indx >= (_num_bits_obst_pattern / 2)) {
game_state.obst_indx = 0;
game_state.obst_pattern = get_random_fuel(game_state.obst_pattern);
}
}
else if (game_state.obst_indx >= _num_bits_obst_pattern - NUM_GRID) {
game_state.obst_indx = 0;
game_state.obst_pattern = get_random_legal(game_state.obst_pattern, state -> difficulty);
}
}
static void update_game(endless_runner_state_t *state, uint8_t subsecond) {
uint8_t curr_jump_frame = 0;
if (game_state.sec_before_moves != 0) {
if (subsecond == 0) --game_state.sec_before_moves;
return;
}
display_obstacles(state);
switch (game_state.jump_state)
{
case NOT_JUMPING:
if (game_state.fuel_mode) {
for (int i = 0; i < JUMP_FRAMES_FUEL_RECHARGE; i++)
{
if(game_state.fuel >= JUMP_FRAMES_FUEL || (state -> difficulty == DIFF_FUEL_1 && !game_state.fuel))
break;
game_state.fuel++;
}
}
break;
case JUMPING_FINAL_FRAME:
stop_jumping(state);
break;
default:
if (game_state.fuel_mode) {
if (!game_state.fuel)
game_state.jump_state = JUMPING_FINAL_FRAME;
else
game_state.fuel--;
if (!HAL_GPIO_BTN_ALARM_read() && !HAL_GPIO_BTN_LIGHT_read()) stop_jumping(state);
}
else {
curr_jump_frame = game_state.jump_state - NOT_JUMPING;
if (curr_jump_frame >= JUMP_FRAMES_EASY || (state -> difficulty >= DIFF_NORM && curr_jump_frame >= JUMP_FRAMES))
game_state.jump_state = JUMPING_FINAL_FRAME;
else
game_state.jump_state++;
}
break;
}
if (game_state.jump_state == NOT_JUMPING && (game_state.loc_2_on || game_state.loc_3_on)) {
delay_ms(200); // To show the player jumping onto the obstacle before displaying the lose screen.
display_lose_screen(state);
}
else if (game_state.fuel_mode)
display_fuel(subsecond, state -> difficulty);
}
void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(endless_runner_state_t));
memset(*context_ptr, 0, sizeof(endless_runner_state_t));
endless_runner_state_t *state = (endless_runner_state_t *)*context_ptr;
state->difficulty = DIFF_NORM;
}
}
void endless_runner_face_activate(void *context) {
(void) context;
}
bool endless_runner_face_loop(movement_event_t event, void *context) {
endless_runner_state_t *state = (endless_runner_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
check_and_reset_hi_score(state);
if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL);
display_title(state);
break;
case EVENT_TICK:
switch (game_state.curr_screen)
{
case SCREEN_TITLE:
case SCREEN_LOSE:
break;
default:
update_game(state, event.subsecond);
break;
}
break;
case EVENT_LIGHT_BUTTON_UP:
case EVENT_ALARM_BUTTON_UP:
if (game_state.curr_screen == SCREEN_TITLE)
begin_playing(state);
else if (game_state.curr_screen == SCREEN_LOSE)
display_title(state);
break;
case EVENT_LIGHT_LONG_PRESS:
if (game_state.curr_screen == SCREEN_TITLE)
change_difficulty(state);
break;
case EVENT_LIGHT_BUTTON_DOWN:
case EVENT_ALARM_BUTTON_DOWN:
if (game_state.curr_screen == SCREEN_PLAYING && game_state.jump_state == NOT_JUMPING){
if (game_state.fuel_mode && !game_state.fuel) break;
game_state.jump_state = JUMPING_START;
display_ball(game_state.jump_state != NOT_JUMPING);
}
break;
case EVENT_ALARM_LONG_PRESS:
if (game_state.curr_screen != SCREEN_PLAYING)
toggle_sound(state);
break;
case EVENT_TIMEOUT:
if (game_state.curr_screen != SCREEN_TITLE)
display_title(state);
break;
case EVENT_LOW_ENERGY_UPDATE:
display_time(watch_rtc_get_date_time(), movement_clock_mode_24h());
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void endless_runner_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,62 @@
/*
* MIT License
*
* Copyright (c) 2024 <David Volovskiy>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef ENDLESS_RUNNER_FACE_H_
#define ENDLESS_RUNNER_FACE_H_
#include "movement.h"
/*
ENDLESS_RUNNER face
This is a basic endless-runner, like the [Chrome Dino game](https://en.wikipedia.org/wiki/Dinosaur_Game).
On the title screen, you can select a difficulty by long-pressing LIGHT or toggle sound by long-pressing ALARM.
LED or ALARM are used to jump.
High-score is displayed on the top-right on the title screen. During a game, the current score is displayed.
*/
typedef struct {
uint16_t hi_score : 10;
uint8_t difficulty : 3;
uint8_t month_last_hi_score : 4;
uint8_t year_last_hi_score : 6;
uint8_t soundOn : 1;
/* 24 bits, likely aligned to 32 bits = 4 bytes */
} endless_runner_state_t;
void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr);
void endless_runner_face_activate(void *context);
bool endless_runner_face_loop(movement_event_t event, void *context);
void endless_runner_face_resign(void *context);
#define endless_runner_face ((const watch_face_t){ \
endless_runner_face_setup, \
endless_runner_face_activate, \
endless_runner_face_loop, \
endless_runner_face_resign, \
NULL, \
})
#endif // ENDLESS_RUNNER_FACE_H_

View File

@@ -0,0 +1,73 @@
/*
* MIT License
*
* Copyright (c) 2023 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 "flashlight_face.h"
void flashlight_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(flashlight_state_t));
memset(*context_ptr, 0, sizeof(flashlight_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.
}
void flashlight_face_activate(void *context) {
(void) context;
HAL_GPIO_A2_out();
HAL_GPIO_A2_clr();
}
bool flashlight_face_loop(movement_event_t event, void *context) {
(void) context;
switch (event.event_type) {
case EVENT_ACTIVATE:
watch_display_string("FL", 0);
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_BUTTON_UP:
HAL_GPIO_A2_toggle();
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void flashlight_face_resign(void *context) {
(void) context;
HAL_GPIO_A2_clr();
HAL_GPIO_A2_off();
}

View File

@@ -0,0 +1,59 @@
/*
* MIT License
*
* Copyright (c) 2023 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 FLASHLIGHT_FACE_H_
#define FLASHLIGHT_FACE_H_
/*
* FLASHLIGHT face
*
* A flashlight for use with the Flashlight sensor board.
*
* When the watch face appears, the display will show "FL" in the top two positions.
* Pressing the Light button will toggle the flashlight on and off.
*
*/
#include "movement.h"
typedef struct {
// Anything you need to keep track of, put it here!
uint8_t unused;
} flashlight_state_t;
void flashlight_face_setup(uint8_t watch_face_index, void ** context_ptr);
void flashlight_face_activate(void *context);
bool flashlight_face_loop(movement_event_t event, void *context);
void flashlight_face_resign(void *context);
#define flashlight_face ((const watch_face_t){ \
flashlight_face_setup, \
flashlight_face_activate, \
flashlight_face_loop, \
flashlight_face_resign, \
NULL, \
})
#endif // FLASHLIGHT_FACE_H_

View File

@@ -0,0 +1,359 @@
/*
* MIT License
*
* Copyright (c) 2023 Tobias Raayoni Last / @randogoth
*
* 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 "toss_up_face.h"
#include "geomancy_face.h"
// CONSTANTS //////////////////////////////////////////////////////////////////
// The Bagua 八卦 Trigrams encoded as 3bit tribbles, represented as binary integer
static const uint32_t bagua = 0b00000101001110010111011100000000;
// The King Wen Sequence 文王卦序 of the I Ching 易經 Hexagrams 卦 encoded as an array
// of decimal integers in the order of two combined Trigram tribbles from 0b000000 to
// 0b111111
static const uint8_t wen_order[] = {
1, 22, 7, 19, 15, 34, 44, 11,
14, 51, 38, 52, 61, 55, 30, 32,
6, 3, 28, 58, 39, 63, 46, 5,
45, 17, 47, 56, 31, 49, 27, 43,
23, 26, 2, 41, 50, 20, 16, 24,
35, 21, 62, 36, 54, 29, 48, 12,
18, 40, 59, 60, 53, 37, 57, 9,
10, 25, 4, 8, 33, 13, 42, 0
};
// The geomantic figures encoded as 4 bit nibbles, represented as hexadecimal integer
static const uint64_t geomantic = 0x4ABF39D25E76C180;
// Abbreviations of the Names of the Geomantic Figures in the order of the 4 bit nibbles
// from 0b0000 to 0b1111
static const char figures[16][2] = {
"VI" /* Via */, "Hd" /* Head of the Dragon */, "PA" /* Puella */, "GF" /* Greater Fortune*/,
"PR" /* Puer */, "AQ" /* Acquisitio */, "CA" /* Carcer */, "TR" /* Tristitia */,
"Td" /* Tail of the Dragon */, "CO" /* Conjunctio */, "AM" /* Amissio */, "AL" /* Albus */,
"LF" /* Lesser Fortune */, "RU" /* Rubeus */, "LA" /* Laetitia */, "PO" /* Populus */
};
// DECLARATIONS ///////////////////////////////////////////////////////////////
static void geomancy_face_display(geomancy_state_t *state);
static nibble_t _geomancy_pick_figure(void);
static tribble_t _iching_pick_trigram(void);
static uint8_t _iching_form_hexagram(void);
static void _geomancy_display(nibble_t code);
static void _display_hexagram(uint8_t hexagram, char* str);
static void _fix_broken_line(uint8_t hexagram);
static void _throw_animation(geomancy_state_t *state);
// WATCH FACE FUNCTIONS ///////////////////////////////////////////////////////
void geomancy_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(geomancy_state_t));
memset(*context_ptr, 0, sizeof(geomancy_state_t));
}
}
void geomancy_face_activate(void *context) {
(void) context;
}
bool geomancy_face_loop(movement_event_t event, void *context) {
geomancy_state_t *state = (geomancy_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
state->animate = false;
state->animation = 0;
watch_display_string(" IChing", 0);
break;
case EVENT_TICK:
if ( state->animate ) {
state->animation = (state->animation + 1) % 39;
geomancy_face_display(state);
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_BUTTON_UP:
if ( state->animate ) break;
if ( state->mode <= 1 ) state->mode = 2;
else if ( state->mode >= 2 ) state->mode = 0;
geomancy_face_display(state);
break;
case EVENT_ALARM_BUTTON_UP:
if ( state->animate ) break;
switch ( state->mode ) {
case 0:
state->mode++;
// fall through
case 1:
state->animate = true;
state->i_ching_hexagram = _iching_form_hexagram();
break;
case 2:
state->mode++;
// fall through
case 3:
state->animate = true;
state->geomantic_figure = _geomancy_pick_figure().bits;
break;
default:
break;
}
geomancy_face_display(state);
break;
case EVENT_ALARM_LONG_PRESS:
if ( state->animate ) break;
state->caption = !state->caption;
watch_display_string(" ", 0);
geomancy_face_display(state);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void geomancy_face_resign(void *context) {
(void) context;
}
// STATIC FUNCTIONS ///////////////////////////////////////////////////////////
/** @brief display handler */
static void geomancy_face_display(geomancy_state_t *state) {
char token[7] = {0};
nibble_t figure = *((nibble_t*) &state->geomantic_figure);
switch ( state->mode ) {
case 0:
watch_display_string(" IChing", 0);
break;
case 1:
_throw_animation(state);
if ( !state->animate ) {
_display_hexagram(state->i_ching_hexagram, token);
watch_display_string(token, 4);
_fix_broken_line(state->i_ching_hexagram);
if (state->caption) {
sprintf(token, "%2d", wen_order[state->i_ching_hexagram] + 1);
watch_display_string(token, 2);
}
}
break;
case 2:
watch_display_string(" GeomCy", 0);
break;
case 3:
_throw_animation(state);
if ( !state->animate ) {
if ( state->caption ) {
sprintf(token, "%c%c", figures[state->geomantic_figure][0], figures[state->geomantic_figure][1]);
watch_display_string(token, 0);
}
_geomancy_display(figure);
}
break;
default:
break;
}
}
/** @brief screen clearing animation between castings */
static void _throw_animation(geomancy_state_t *state) {
movement_request_tick_frequency(16);
switch ( state->animation ) {
case 0:
watch_set_pixel(0, 22);
break;
case 1:
watch_set_pixel(2, 22);
watch_set_pixel(2, 23);
watch_clear_pixel(0, 22);
break;
case 2:
watch_set_pixel(1, 22);
watch_set_pixel(0, 23);
break;
case 3:
watch_set_pixel(2, 0);
watch_set_pixel(1, 0);
watch_set_pixel(2, 21);
watch_set_pixel(1, 21);
watch_clear_pixel(2, 22);
watch_clear_pixel(1, 22);
watch_clear_pixel(2, 23);
watch_clear_pixel(0, 23);
watch_clear_pixel(1, 23);
break;
case 4:
watch_set_pixel(1, 17);
watch_set_pixel(0, 20);
watch_set_pixel(2, 10);
watch_set_pixel(0, 1);
break;
case 5:
watch_clear_pixel(2, 21);
watch_clear_pixel(1, 21);
watch_clear_pixel(2, 0);
watch_clear_pixel(1, 0);
watch_clear_pixel(1, 20);
watch_clear_pixel(2, 20);
watch_clear_pixel(0, 21);
watch_clear_pixel(1, 1);
watch_clear_pixel(0, 0);
watch_clear_pixel(2, 1);
watch_set_pixel(2, 19);
watch_set_pixel(0, 19);
watch_set_pixel(1, 2);
watch_set_pixel(0, 2);
break;
case 6:
watch_clear_pixel(1, 17);
watch_clear_pixel(0, 20);
watch_clear_pixel(2, 10);
watch_clear_pixel(0, 1);
watch_set_pixel(2, 18);
watch_set_pixel(0, 18);
watch_set_pixel(2, 3);
watch_set_pixel(0, 4);
break;
case 7:
watch_clear_pixel(2, 19);
watch_clear_pixel(0, 19);
watch_clear_pixel(1, 18);
watch_clear_pixel(1, 19);
watch_clear_pixel(1, 2);
watch_clear_pixel(0, 2);
watch_clear_pixel(1, 3);
watch_clear_pixel(0, 3);
watch_clear_pixel(2, 2);
watch_set_pixel(1, 4);
watch_set_pixel(0, 5);
break;
case 8:
watch_clear_pixel(2, 18);
watch_clear_pixel(0, 18);
watch_clear_pixel(2, 3);
watch_clear_pixel(0, 4);
watch_set_pixel(2, 5);
watch_set_pixel(1, 6);
break;
case 9:
watch_clear_pixel(1, 4);
watch_clear_pixel(0, 5);
watch_clear_pixel(1, 5);
watch_clear_pixel(2, 4);
watch_clear_pixel(0, 6);
break;
case 10:
watch_clear_pixel(2, 5);
watch_clear_pixel(1, 6);
break;
case 11:
state->animate = false;
state->animation = 0;
movement_request_tick_frequency(1);
break;
}
}
// I CHING FUNCTIONS //////////////////////////////////////////////////////////
/** @brief form a trigram from three random bit picks
*/
static tribble_t _iching_pick_trigram(void) {
uint8_t index = (divine_bit() << 2) | (divine_bit() << 1) | divine_bit();
tribble_t trigram = {(bagua >> (3 * index)) & 0b111};
return trigram;
}
/** @brief form a hexagram from two trigrams
*/
static uint8_t _iching_form_hexagram(void);
static uint8_t _iching_form_hexagram(void) {
tribble_t inner = _iching_pick_trigram();
tribble_t outer = _iching_pick_trigram();
uint8_t hexagram = (inner.bits << 3) | outer.bits;
return hexagram;
}
/** @brief display hexagram
* @details | for unbroken lines and Ξ for broken lines, left of display is bottom
*/
static void _display_hexagram(uint8_t hexagram, char* str);
static void _display_hexagram(uint8_t hexagram, char* str) {
str[6] = '\0'; // Null-terminate the string
for (uint8_t i = 0; i < 6; i++) {
if (hexagram & (1 << (5 - i))) {
str[i] = '1';
} else {
str[i] = '=';
}
}
}
/** @brief when Ξ digits show as = then manually add a line on top
*/
static void _fix_broken_line(uint8_t hexagram) {
for (uint8_t i = 0; i < 6; i++) {
if (!(hexagram & (1 << (5 - i)))) {
if ( i == 1 ) watch_set_pixel(2, 20);
if ( i == 3 ) watch_set_pixel(2, 1);
if ( i == 4 ) watch_set_pixel(2, 2);
if ( i == 5 ) watch_set_pixel(2, 4);
}
}
}
// GEOMANCY FUNCTIONS /////////////////////////////////////////////////////////
/** @brief choose a geomantic figure from four random bits
* @details 0 represents · and 1 represents : counting from the bottom
*/
static nibble_t _geomancy_pick_figure(void);
static nibble_t _geomancy_pick_figure(void) {
uint8_t index = (divine_bit() << 3) | (divine_bit() << 2) | (divine_bit() << 1) | divine_bit();
nibble_t figure = {(geomantic >> (4 * (15 - index))) & 0xF};
return figure;
}
/** @brief display the geomantic figure, left of display is bottom
*/
static void _geomancy_display(nibble_t code) {
// draw geomantic figures
bool row1 = (code.bits >> 3) & 1;
bool row2 = (code.bits >> 2) & 1;
bool row3 = (code.bits >> 1) & 1;
bool row4 = code.bits & 1;
if ( row1 ) watch_set_pixel(1, 18); else watch_set_pixel(1, 19);
if ( row2 ) { watch_set_pixel(2, 20); watch_set_pixel(0, 21);} else watch_set_pixel(1, 20);
if ( row3 ) watch_set_pixel(0, 22); else watch_set_pixel(1, 23);
if ( row4 ) { watch_set_pixel(2, 1); watch_set_pixel(0, 0);} else watch_set_pixel(1, 1);
}

View File

@@ -0,0 +1,99 @@
/*
* MIT License
*
* Copyright (c) 2023 Tobias Raayoni Last / @randogoth
*
* 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 GEOMANCY_FACE_H_
#define GEOMANCY_FACE_H_
/*
* GEOMANCY watch face
*
* A simple and straightforward watch face for the ancient Eastern geomantic divination system
* of I Ching and the western system of "Geomancy". It is an optional addition to the Toss Up
* Face.
*
* The LIGHT button toggles between the two systems of geomancy.
*
* The ALARM button casts an I Ching hexagram or Geomantic figure based on drawing virtual
* stalks from the True Random Number Generator in the Sensor Watch.
*
* The figures are flipped 90 degrees clockwise, so the left side is the bottom and the
* right side the top.
*
* LONG PRESSING ALARM toggles the display of the King Wen sequence index for the cast I Ching
* Hexagram (https://en.wikipedia.org/wiki/King_Wen_sequence )or the abbreviated name for the
* cast Geomantic Figure:
*
* GF - Greater Fortune (Fortuna Major)
* LF - Lesser Fortune (Fortuna Minor)
* PO - Populus
* VI - Via
* AL - Albus
* CO - Conjunctio
* PA - Puella
* AM - Amissio
* PR - Puer
* RU - Rubeus
* AQ - Acquisitio
* LA - Laetitia
* TR - Tristitia
* CA - Carcer
* HD - Head of the Dragon (Caput Draconis)
* TD - Tail of the Dragon (Cauda Draconis)
*
*/
#include "movement.h"
typedef struct {
uint8_t bits : 4;
} nibble_t;
typedef struct {
uint8_t bits : 3;
} tribble_t;
typedef struct {
uint8_t mode : 3;
uint8_t geomantic_figure;
uint8_t i_ching_hexagram : 6;
bool caption;
uint8_t animation;
bool animate;
} geomancy_state_t;
void geomancy_face_setup(uint8_t watch_face_index, void ** context_ptr);
void geomancy_face_activate(void *context);
bool geomancy_face_loop(movement_event_t event, void *context);
void geomancy_face_resign(void *context);
#define geomancy_face ((const watch_face_t){ \
geomancy_face_setup, \
geomancy_face_activate, \
geomancy_face_loop, \
geomancy_face_resign, \
NULL, \
})
#endif // GEOMANCY_FACE_H_

View File

@@ -0,0 +1,154 @@
/*
* MIT License
*
* Copyright (c) 2023 tslil clingman
*
* 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 "habit_face.h"
#include "watch_private_display.h"
#include "watch_rtc.h"
#include "watch_slcd.h"
#include "watch_utility.h"
#include <stdlib.h>
#include <string.h>
static inline uint32_t today_unix(const uint32_t utc_offset) {
const watch_date_time_t dt = watch_rtc_get_date_time();
return watch_utility_convert_to_unix_time(dt.unit.year + 2020, dt.unit.month,
dt.unit.day, 0, 0, 0, utc_offset);
}
static inline uint32_t days_since_unix(const uint32_t since,
const uint32_t until) {
return (until - since) / (60 * 60 * 24);
}
typedef struct {
uint16_t total_count;
uint8_t lookback;
uint32_t last_update;
bool display_total;
} habit_state_t;
void habit_face_setup(uint8_t watch_face_index,
void **context_ptr) {
(void)watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(habit_state_t));
memset(*context_ptr, 0, sizeof(habit_state_t));
habit_state_t *state = (habit_state_t *)*context_ptr;
state->lookback = 0;
state->last_update = watch_utility_offset_timestamp(
today_unix(movement_get_current_timezone_offset()), -24, 0, 0);
}
}
static inline void display_state(habit_state_t *state) {
const bool can_do = (state->lookback & 1) == 0;
char buf[16];
if (can_do) {
watch_clear_indicator(WATCH_INDICATOR_LAP);
} else {
watch_set_indicator(WATCH_INDICATOR_LAP);
}
if (state->display_total) {
sprintf(buf, "HA %03dtot", state->total_count);
watch_display_string(buf, 0);
} else {
sprintf(buf, "HA d%c", can_do ? 'o' : 'n');
uint8_t copy = state->lookback;
for (uint8_t c = 0; copy; copy >>= 2, c++) {
switch (copy & 3) {
case 0:
break;
case 1:
buf[4 + c] = 'I';
break;
case 2:
buf[4 + c] = '1';
break;
case 3:
buf[4 + c] = '|';
break;
}
}
watch_display_string(buf, 0);
if (can_do) {
const uint64_t segmap = Segment_Map[4] >> 48;
watch_set_pixel((segmap & 0xFF) >> 6, segmap & 0x3F);
}
}
}
void habit_face_activate(void *context) {
habit_state_t *state = (habit_state_t *)context;
display_state(state);
}
bool habit_face_loop(movement_event_t event,
void *context) {
habit_state_t *state = (habit_state_t *)context;
const uint32_t today_now_unix = today_unix(movement_get_current_timezone_offset());
const bool can_do = (state->lookback & 1) == 0;
switch (event.event_type) {
case EVENT_ACTIVATE:
case EVENT_TICK: {
display_state(state);
if (today_now_unix > state->last_update) {
uint8_t num_shifts = days_since_unix(state->last_update, today_now_unix);
if (num_shifts > 7)
num_shifts = 7;
state->lookback <<= num_shifts;
state->last_update = today_now_unix;
}
break;
}
case EVENT_LIGHT_BUTTON_UP: {
state->display_total = !state->display_total;
display_state(state);
break;
}
case EVENT_ALARM_BUTTON_UP: {
if (can_do) {
state->lookback |= 1;
state->total_count++;
state->last_update = today_now_unix;
display_state(state);
};
break;
}
case EVENT_TIMEOUT: {
movement_move_to_face(0);
break;
}
default:
return movement_default_loop_handler(event);
}
return true;
}
void habit_face_resign(void *context) {
(void)context;
}

View File

@@ -0,0 +1,53 @@
/*
* MIT License
*
* Copyright (c) 2023 tslil clingman
*
* 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 HABIT_FACE_H_
#define HABIT_FACE_H_
/*
* Habit tracking face
*
* Allows the user to record a single succesful instance of a particular habit
* occuring per day, and displays history for eight days prior as well as a
* total counter.
*
*/
#include "movement.h"
void habit_face_setup(uint8_t watch_face_index,
void **context_ptr);
void habit_face_activate(void *context);
bool habit_face_loop(movement_event_t event, void *context);
void habit_face_resign(void *context);
#define habit_face ((const watch_face_t){ \
habit_face_setup, \
habit_face_activate, \
habit_face_loop, \
habit_face_resign, \
NULL, \
})
#endif // HABIT_FACE_H_

View File

@@ -0,0 +1,393 @@
/*
* MIT License
*
* Copyright (c) 2023 Chris Ellis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Emulator only: need time() to seed the random number generator.
#if __EMSCRIPTEN__
#include <time.h>
#endif
#include <stdlib.h>
#include <string.h>
#include "higher_lower_game_face.h"
#include "watch_private_display.h"
#define TITLE_TEXT "Hi-Lo"
#define GAME_BOARD_SIZE 6
#define MAX_BOARDS 40
#define GUESSES_PER_SCREEN 5
#define WIN_SCORE (MAX_BOARDS * GUESSES_PER_SCREEN)
#define STATUS_DISPLAY_START 0
#define BOARD_SCORE_DISPLAY_START 2
#define BOARD_DISPLAY_START 4
#define BOARD_DISPLAY_END 9
#define MIN_CARD_VALUE 2
#define MAX_CARD_VALUE 14
#define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1)
#define CARD_SUIT_COUNT 4
#define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT)
#define FLIP_BOARD_DIRECTION false
typedef struct card_t {
uint8_t value;
bool revealed;
} card_t;
typedef enum {
A, B, C, D, E, F, G
} segment_t;
typedef enum {
HL_GUESS_EQUAL,
HL_GUESS_HIGHER,
HL_GUESS_LOWER
} guess_t;
typedef enum {
HL_GS_TITLE_SCREEN,
HL_GS_GUESSING,
HL_GS_WIN,
HL_GS_LOSE,
HL_GS_SHOW_SCORE,
} game_state_t;
static game_state_t game_state = HL_GS_TITLE_SCREEN;
static card_t game_board[GAME_BOARD_SIZE] = {0};
static uint8_t guess_position = 0;
static uint8_t score = 0;
static uint8_t completed_board_count = 0;
static uint8_t deck[DECK_SIZE] = {0};
static uint8_t current_card = 0;
static uint8_t generate_random_number(uint8_t num_values) {
// Emulator: use rand. Hardware: use arc4random.
#if __EMSCRIPTEN__
return rand() % num_values;
#else
return arc4random_uniform(num_values);
#endif
}
static void stack_deck(void) {
for (size_t i = 0; i < CARD_RANK_COUNT; i++) {
for (size_t j = 0; j < CARD_SUIT_COUNT; j++)
deck[(i * CARD_SUIT_COUNT) + j] = MIN_CARD_VALUE + i;
}
}
static void shuffle_deck(void) {
// Randomize shuffle with Fisher Yates
size_t i;
size_t j;
uint8_t tmp;
for (i = DECK_SIZE - 1; i > 0; i--) {
j = generate_random_number(0xFF) % (i + 1);
tmp = deck[j];
deck[j] = deck[i];
deck[i] = tmp;
}
}
static void reset_deck(void) {
current_card = 0;
stack_deck();
shuffle_deck();
}
static uint8_t get_next_card(void) {
if (current_card >= DECK_SIZE)
reset_deck();
return deck[current_card++];
}
static void reset_board(bool first_round) {
// First card is random on the first board, and carried over from the last position on subsequent boards
const uint8_t first_card_value = first_round
? get_next_card()
: game_board[GAME_BOARD_SIZE - 1].value;
game_board[0].value = first_card_value;
game_board[0].revealed = true; // Always reveal first card
// Fill remainder of board
for (size_t i = 1; i < GAME_BOARD_SIZE; ++i) {
game_board[i] = (card_t) {
.value = get_next_card(),
.revealed = false
};
}
}
static void init_game(void) {
watch_clear_display();
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);
watch_display_string("GA", STATUS_DISPLAY_START);
reset_deck();
reset_board(true);
score = 0;
completed_board_count = 0;
guess_position = 1;
}
static void set_segment_at_position(segment_t segment, uint8_t position) {
const uint64_t position_segment_data = (Segment_Map[position] >> (8 * (uint8_t) segment)) & 0xFF;
const uint8_t com_pin = position_segment_data >> 6;
const uint8_t seg = position_segment_data & 0x3F;
watch_set_pixel(com_pin, seg);
}
static void render_board_position(size_t board_position) {
const size_t display_position = FLIP_BOARD_DIRECTION
? BOARD_DISPLAY_START + board_position
: BOARD_DISPLAY_END - board_position;
const bool revealed = game_board[board_position].revealed;
//// Current position indicator spot
//if (board_position == guess_position) {
// watch_display_character('-', display_position);
// return;
//}
if (!revealed) {
// Higher or lower indicator (currently just an empty space)
watch_display_character(' ', display_position);
//set_segment_at_position(F, display_position);
return;
}
const uint8_t value = game_board[board_position].value;
switch (value) {
case 14: // A (≡)
watch_display_character(' ', display_position);
set_segment_at_position(A, display_position);
set_segment_at_position(D, display_position);
set_segment_at_position(G, display_position);
break;
case 13: // K (=)
watch_display_character(' ', display_position);
set_segment_at_position(A, display_position);
set_segment_at_position(D, display_position);
break;
case 12: // Q (-)
watch_display_character('-', display_position);
break;
default: {
const char display_char = (value - MIN_CARD_VALUE) + '0';
watch_display_character(display_char, display_position);
}
}
}
static void render_board(void) {
for (size_t i = 0; i < GAME_BOARD_SIZE; ++i) {
render_board_position(i);
}
}
static void render_board_count(void) {
// Render completed boards (screens)
char buf[3] = {0};
snprintf(buf, sizeof(buf), "%2hhu", completed_board_count);
watch_display_string(buf, BOARD_SCORE_DISPLAY_START);
}
static void render_final_score(void) {
watch_display_string("SC", STATUS_DISPLAY_START);
char buf[7] = {0};
const uint8_t complete_boards = score / GUESSES_PER_SCREEN;
snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score);
watch_set_colon();
watch_display_string(buf, BOARD_DISPLAY_START);
}
static guess_t get_answer(void) {
if (guess_position < 1 || guess_position > GAME_BOARD_SIZE)
return HL_GUESS_EQUAL; // Maybe add an error state, shouldn't ever hit this.
game_board[guess_position].revealed = true;
const uint8_t previous_value = game_board[guess_position - 1].value;
const uint8_t current_value = game_board[guess_position].value;
if (current_value > previous_value)
return HL_GUESS_HIGHER;
else if (current_value < previous_value)
return HL_GUESS_LOWER;
else
return HL_GUESS_EQUAL;
}
static void do_game_loop(guess_t user_guess) {
switch (game_state) {
case HL_GS_TITLE_SCREEN:
init_game();
render_board();
render_board_count();
game_state = HL_GS_GUESSING;
break;
case HL_GS_GUESSING: {
const guess_t answer = get_answer();
// Render answer indicator
switch (answer) {
case HL_GUESS_EQUAL:
watch_display_string("==", STATUS_DISPLAY_START);
break;
case HL_GUESS_HIGHER:
watch_display_string("HI", STATUS_DISPLAY_START);
break;
case HL_GUESS_LOWER:
watch_display_string("LO", STATUS_DISPLAY_START);
break;
}
// Scoring
if (answer == user_guess) {
score++;
} else if (answer == HL_GUESS_EQUAL) {
// No score for two consecutive identical cards
} else {
// Incorrect guess, game over
watch_display_string("GO", STATUS_DISPLAY_START);
game_board[guess_position].revealed = true;
render_board_position(guess_position);
game_state = HL_GS_LOSE;
return;
}
if (score >= WIN_SCORE) {
// Win, perhaps some kind of animation sequence?
watch_display_string("WI", STATUS_DISPLAY_START);
watch_display_string(" ", BOARD_SCORE_DISPLAY_START);
watch_display_string("------", BOARD_DISPLAY_START);
game_state = HL_GS_WIN;
return;
}
// Next guess position
const bool final_board_guess = guess_position == GAME_BOARD_SIZE - 1;
if (final_board_guess) {
// Seed new board
completed_board_count++;
render_board_count();
guess_position = 1;
reset_board(false);
render_board();
} else {
guess_position++;
render_board_position(guess_position - 1);
render_board_position(guess_position);
}
break;
}
case HL_GS_WIN:
case HL_GS_LOSE:
// Show score screen on button press from either state
watch_clear_display();
render_final_score();
game_state = HL_GS_SHOW_SCORE;
break;
case HL_GS_SHOW_SCORE:
watch_clear_display();
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);
watch_display_string("GA", STATUS_DISPLAY_START);
game_state = HL_GS_TITLE_SCREEN;
break;
default:
watch_display_string("ERROR", BOARD_DISPLAY_START);
break;
}
}
static void light_button_handler(void) {
do_game_loop(HL_GUESS_HIGHER);
}
static void alarm_button_handler(void) {
do_game_loop(HL_GUESS_LOWER);
}
void higher_lower_game_face_setup(uint8_t watch_face_index, void **context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(higher_lower_game_face_state_t));
memset(*context_ptr, 0, sizeof(higher_lower_game_face_state_t));
// Do any one-time tasks in here; the inside of this conditional happens only at boot.
memset(game_board, 0, sizeof(game_board));
}
// Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.
}
void higher_lower_game_face_activate(void *context) {
higher_lower_game_face_state_t *state = (higher_lower_game_face_state_t *) context;
(void) state;
// Handle any tasks related to your watch face coming on screen.
game_state = HL_GS_TITLE_SCREEN;
}
bool higher_lower_game_face_loop(movement_event_t event, void *context) {
higher_lower_game_face_state_t *state = (higher_lower_game_face_state_t *) context;
(void) state;
switch (event.event_type) {
case EVENT_ACTIVATE:
// Show your initial UI here.
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);
watch_display_string("GA", STATUS_DISPLAY_START);
break;
case EVENT_TICK:
// If needed, update your display here.
break;
case EVENT_LIGHT_BUTTON_UP:
light_button_handler();
break;
case EVENT_LIGHT_BUTTON_DOWN:
// Don't trigger light
break;
case EVENT_ALARM_BUTTON_UP:
alarm_button_handler();
break;
case EVENT_TIMEOUT:
// Your watch face will receive this event after a period of inactivity. If it makes sense to resign,
// you may uncomment this line to move back to the first watch face in the list:
// movement_move_to_face(0);
break;
default:
return movement_default_loop_handler(event);
}
// return true if the watch can enter standby mode. Generally speaking, you should always return true.
// Exceptions:
// * If you are displaying a color using the low-level watch_set_led_color function, you should return false.
// * If you are sounding the buzzer using the low-level watch_set_buzzer_on function, you should return false.
// Note that if you are driving the LED or buzzer using Movement functions like movement_illuminate_led or
// movement_play_alarm, you can still return true. This guidance only applies to the low-level watch_ functions.
return true;
}
void higher_lower_game_face_resign(void *context) {
(void) context;
// handle any cleanup before your watch face goes off-screen.
}

View File

@@ -0,0 +1,106 @@
/*
* MIT License
*
* Copyright (c) 2023 Chris Ellis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef HIGHER_LOWER_GAME_FACE_H_
#define HIGHER_LOWER_GAME_FACE_H_
#include "movement.h"
/*
* Higher-Lower game face
* ======================
*
* A game face based on the "higher-lower" card game where the objective is to correctly guess if the next card will
* be higher or lower than the last revealed cards.
*
* Game Flow:
* - When the face is selected, the "Hi-Lo" "Title" screen will be displayed, and the status indicator will display "GA" for game
* - Pressing `ALARM` or `LIGHT` will start the game and proceed to the "Guessing" screen
* - The first card will be revealed and the player must now make a guess
* - A player can guess `Higher` by pressing the `LIGHT` button, and `Lower` by pressing the `ALARM` button
* - The status indicator will show the result of the guess: HI (Higher), LO (Lower), or == (Equal)
* - There are five guesses to make on each game screen, once the end of the screen is reached, a new screen
* will be started, with the last revealed card carried over
* - The number of completed screens is displayed in the top right (see Scoring)
* - If the player has guessed correctly, the score is updated and play continues (see Scoring)
* - If the player has guessed incorrectly, the status will change to GO (Game Over)
* - The current card will be revealed
* - Pressing `ALARM` or `LIGHT` will transition to the "Score" screen
* - If the game is won, the status indicator will display "WI" and the "Win" screen will be displayed
* - Pressing `ALARM` or `LIGHT` will transition to the "Score" screen
* - The status indicator will change to "SC" when the final score is displayed
* - The number of completed game screens will be displayed on using the first two digits
* - The number of correct guesses will be displayed using the final three digits
* - E.g. "13: 063" represents 13 completed screens, with 63 correct guesses
* - Pressing `ALARM` or `LIGHT` while on the "Score" screen will transition to back to the "Title" screen
*
* Scoring:
* - If the player guesses correctly (HI/LO) a point is gained
* - If the player guesses incorrectly the game ends
* - Unless the revealed card is equal (==) to the last card, in which case play continues, but no point is gained
* - If the player completes 40 screens full of cards, the game ends and a win screen is displayed
*
* Misc:
* The face tries to remain true to the spirit of using "cards"; to cope with the display limitations I've arrived at
* the following mapping of card values to screen display, but am open to better suggestions:
*
* Thanks to voloved for adding deck shuffling and drawing!
*
* | Cards | |
* |---------|--------------------------|
* | Value |2|3|4|5|6|7|8|9|10|J|Q|K|A|
* | Display |0|1|2|3|4|5|6|7|8 |9|-|=|≡|
*
* A previous alternative can be found in the git history:
* | Cards | |
* |---------|--------------------------|
* | Value |2|3|4|5|6|7|8|9|10|J|Q|K|A|
* | Display |2|3|4|5|6|7|8|9| 0|-|=|≡|H|
*
*
* Future Ideas:
* - Add sounds
* - Save/Display high score
* - Add a "Win" animation
* - Consider using lap indicator for larger score limit
*/
typedef struct {
// Anything you need to keep track of, put it here!
} higher_lower_game_face_state_t;
void higher_lower_game_face_setup(uint8_t watch_face_index, void ** context_ptr);
void higher_lower_game_face_activate(void *context);
bool higher_lower_game_face_loop(movement_event_t event, void *context);
void higher_lower_game_face_resign(void *context);
#define higher_lower_game_face ((const watch_face_t){ \
higher_lower_game_face_setup, \
higher_lower_game_face_activate, \
higher_lower_game_face_loop, \
higher_lower_game_face_resign, \
NULL, \
})
#endif // HIGHER_LOWER_GAME_FACE_H_

View File

@@ -0,0 +1,622 @@
/*
* MIT License
*
* Copyright (c) 2022 Andreas Nebinger
*
* 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 "interval_face.h"
#include "watch.h"
#include "watch_utility.h"
#include "watch_private_display.h"
#include "watch_buzzer.h"
typedef enum {
interval_setting_0_timer_idx,
interval_setting_1_clear_yn,
interval_setting_2_warmup_minutes,
interval_setting_3_warmup_seconds,
interval_setting_4_work_minutes,
interval_setting_5_work_seconds,
interval_setting_6_work_rounds,
interval_setting_7_break_minutes,
interval_setting_8_break_seconds,
interval_setting_9_full_rounds,
interval_setting_10_cooldown_minutes,
interval_setting_11_cooldown_seconds,
interval_setting_max
} interval_setting_idx_t;
#define INTERVAL_FACE_STATE_DEFAULT "IT" // Interval Timer
#define INTERVAL_FACE_STATE_WARMUP "PR" // PRepare / warm up
#define INTERVAL_FACE_STATE_WORK "WO" // WOrk
#define INTERVAL_FACE_STATE_BREAK "BR" // BReak
#define INTERVAL_FACE_STATE_COOLDOWN "CD" // CoolDown
// Define some default timer settings. Each timer is described in an array like this:
// 1. warm-up seconds,
// 2. work time (seconds/minutes)
// 3. break time (seconds/minutes)
// 4. full rounds (0 = no limit)
// 5. cooldown seconds
// Work time and break time: positive number = seconds, negative number = minutes
static const int8_t _default_timers[6][5] = {{0, 40, 20, 0, 0},
{0, 45, 15, 0, 0},
{10, 20, 10, 8, 10},
{0, 35, 0, 0, 0},
{0, -25, -5, 0, 0},
{0, -20, -5, 0, 0}};
static const uint8_t _intro_segdata[4][2] = {{1, 8}, {0, 8}, {0, 7}, {1, 7}};
static const uint8_t _blink_idx[] = {3, 9, 4, 6, 4, 6, 8, 4, 6, 8, 4, 6};
static const uint8_t _setting_page_idx[] = {1, 0, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4};
static const int8_t _sound_seq_warmup[] = {BUZZER_NOTE_F6, 8, BUZZER_NOTE_REST, 1, -2, 3, 0};
static const int8_t _sound_seq_work[] = {BUZZER_NOTE_F6, 8, BUZZER_NOTE_REST, 1, -2, 2, BUZZER_NOTE_C7, 24, 0};
static const int8_t _sound_seq_break[] = {BUZZER_NOTE_B6, 15, BUZZER_NOTE_REST, 1, -2, 1, BUZZER_NOTE_B6, 16, 0};
static const int8_t _sound_seq_cooldown[] = {BUZZER_NOTE_C7, 15, BUZZER_NOTE_REST, 1, -2, 1, BUZZER_NOTE_C7, 24, 0};
static const int8_t _sound_seq_finish[] = {BUZZER_NOTE_C7, 6, BUZZER_NOTE_E7, 6, BUZZER_NOTE_G7, 6, BUZZER_NOTE_C8, 18, 0};
static interval_setting_idx_t _setting_idx;
static int8_t _ticks;
static bool _erase_timer_flag;
static uint32_t _target_ts;
static uint32_t _now_ts;
static uint32_t _paused_ts;
static uint8_t _timer_work_round;
static uint8_t _timer_full_round;
static uint8_t _timer_run_state;
static inline void _inc_uint8(uint8_t *value, uint8_t step, uint8_t max) {
*value += step;
if (*value >= max) *value = 0;
}
static uint32_t _get_now_ts() {
// returns the current date time as unix timestamp
watch_date_time_t now = watch_rtc_get_date_time();
return watch_utility_date_time_to_unix_time(now, 0);
}
static inline void _button_beep() {
// play a beep as confirmation for a button press (if applicable)
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
}
static void _timer_write_info(interval_face_state_t *state, char *buf, uint8_t timer_page) {
// fill display string with requested timer information
switch (timer_page) {
case 0:
// clear timer?
sprintf(buf, "%2s %1dCLEARn", INTERVAL_FACE_STATE_DEFAULT, state->timer_idx + 1);
if (_erase_timer_flag) buf[9] = 'y';
watch_clear_colon();
break;
case 1:
// warmup time info
sprintf(buf, "%2s %1d%02d%02d ", INTERVAL_FACE_STATE_WARMUP, state->timer_idx + 1,
state->timer[state->timer_idx].warmup_minutes,
state->timer[state->timer_idx].warmup_seconds);
break;
case 2:
// work interval info
sprintf(buf, "%2s %1d%02d%02d%2d", INTERVAL_FACE_STATE_WORK, state->timer_idx + 1,
state->timer[state->timer_idx].work_minutes,
state->timer[state->timer_idx].work_seconds,
state->timer[state->timer_idx].work_rounds);
break;
case 3:
// break interval info
sprintf(buf, "%2s %1d%02d%02d%2d", INTERVAL_FACE_STATE_BREAK, state->timer_idx + 1,
state->timer[state->timer_idx].break_minutes,
state->timer[state->timer_idx].break_seconds,
state->timer[state->timer_idx].full_rounds);
if (!state->timer[state->timer_idx].full_rounds) buf[9] = '-';
break;
case 4:
// cooldown time info
sprintf(buf, "%2s %1d%02d%02d ", INTERVAL_FACE_STATE_COOLDOWN ,state->timer_idx + 1,
state->timer[state->timer_idx].cooldown_minutes,
state->timer[state->timer_idx].cooldown_seconds);
break;
default:
break;
}
}
static void _face_draw(interval_face_state_t *state, uint8_t subsecond) {
// draws current face state
if (!state->is_active) return;
char buf[14];
buf[0] = 0;
uint8_t tmp;
if (state->face_state == interval_state_waiting && _ticks >= 0) {
// play info slideshow for current timer
int8_t ticks = _ticks % 12;
if (ticks == 0) {
if ((state->timer[state->timer_idx].warmup_minutes + state->timer[state->timer_idx].warmup_seconds) == 0) {
// skip warmup info if there is none for this timer
ticks = 3;
_ticks += 3;
}
}
tmp = ticks / 3 + 1;
_timer_write_info(state, buf, tmp);
// don't show '1 round' when displaying workout time to avoid detail overload
if (tmp == 2 && state->timer[state->timer_idx].work_rounds == 1) buf[9] = ' ';
// blink colon
if (subsecond % 2 == 0 && _ticks < 24) watch_clear_colon();
else watch_set_colon();
} else if (state->face_state == interval_state_setting) {
if (_setting_idx == interval_setting_0_timer_idx) {
if ((state->timer[state->timer_idx].warmup_minutes + state->timer[state->timer_idx].warmup_seconds) == 0)
tmp = 1;
else
tmp = 2;
} else {
tmp = _setting_page_idx[_setting_idx];
}
_timer_write_info(state, buf, tmp);
// blink at cursor position
if (subsecond % 2 && _ticks != -2) {
buf[_blink_idx[_setting_idx]] = ' ';
if (_blink_idx[_setting_idx] % 2 == 0) buf[_blink_idx[_setting_idx] + 1] = ' ';
}
// show lap indicator only when rounds are set
if (_setting_idx == interval_setting_6_work_rounds || _setting_idx == interval_setting_9_full_rounds)
watch_set_indicator(WATCH_INDICATOR_LAP);
else
watch_clear_indicator(WATCH_INDICATOR_LAP);
} else if (state->face_state == interval_state_running || state->face_state == interval_state_pausing) {
tmp = _timer_full_round;
switch (_timer_run_state) {
case 0:
sprintf(buf, INTERVAL_FACE_STATE_WARMUP);
break;
case 1:
sprintf(buf, INTERVAL_FACE_STATE_WORK);
if (state->timer[state->timer_idx].work_rounds > 1) tmp = _timer_work_round;
break;
case 2:
sprintf(buf, INTERVAL_FACE_STATE_BREAK);
break;
case 3:
sprintf(buf, INTERVAL_FACE_STATE_COOLDOWN);
break;
default:
break;
}
div_t delta;
if (state->face_state == interval_state_pausing) {
// pausing
delta = div(_target_ts - _paused_ts, 60);
// blink the bell icon
if (_now_ts % 2) watch_set_indicator(WATCH_INDICATOR_BELL);
else watch_clear_indicator(WATCH_INDICATOR_BELL);
} else
// running
delta = div(_target_ts - _now_ts, 60);
sprintf(&buf[2], " %1d%02d%02d%2d", state->timer_idx + 1, delta.quot, delta.rem, tmp + 1);
}
// write out to lcd
if (buf[0]) {
watch_display_character(buf[0], 0);
watch_display_character(buf[1], 1);
// set the bar for the i-like symbol on position 2
watch_set_pixel(2, 9);
// display the rest of the string
watch_display_string(&buf[3], 3);
}
}
static void _initiate_setting(interval_face_state_t *state, uint8_t subsecond) {
state->face_state = interval_state_setting;
_setting_idx = interval_setting_0_timer_idx;
_ticks = 0;
_erase_timer_flag = false;
watch_set_colon();
movement_request_tick_frequency(4);
_face_draw(state, subsecond);
}
static void _resume_setting(interval_face_state_t *state, uint8_t subsecond) {
state->face_state = interval_state_waiting;
_ticks = 0;
_face_draw(state, subsecond);
movement_request_tick_frequency(2);
watch_clear_indicator(WATCH_INDICATOR_LAP);
}
static void _abort_quick_ticks() {
if (_ticks == -2) {
_ticks = -1;
movement_request_tick_frequency(4);
}
}
static void _handle_alarm_button(interval_face_state_t *state) {
// handles the alarm button press and alters the corresponding timer settings
switch (_setting_idx) {
case interval_setting_0_timer_idx:
_inc_uint8(&state->timer_idx, 1, INTERVAL_TIMERS);
_erase_timer_flag = false;
break;
case interval_setting_1_clear_yn:
_erase_timer_flag ^= 1;
break;
case interval_setting_2_warmup_minutes:
_inc_uint8(&state->timer[state->timer_idx].warmup_minutes, 1, 60);
break;
case interval_setting_3_warmup_seconds:
_inc_uint8(&state->timer[state->timer_idx].warmup_seconds, 5, 60);
break;
case interval_setting_4_work_minutes:
_inc_uint8(&state->timer[state->timer_idx].work_minutes, 1, 60);
if (state->timer[state->timer_idx].work_rounds == 0) state->timer[state->timer_idx].work_rounds = 1;
break;
case interval_setting_5_work_seconds:
_inc_uint8(&state->timer[state->timer_idx].work_seconds, 5, 60);
if (state->timer[state->timer_idx].work_rounds == 0) state->timer[state->timer_idx].work_rounds = 1;
break;
case interval_setting_6_work_rounds:
_inc_uint8(&state->timer[state->timer_idx].work_rounds, 1, 100);
break;
case interval_setting_7_break_minutes:
_inc_uint8(&state->timer[state->timer_idx].break_minutes, 1, 60);
break;
case interval_setting_8_break_seconds:
_inc_uint8(&state->timer[state->timer_idx].break_seconds, 5, 60);
break;
case interval_setting_9_full_rounds:
_inc_uint8(&state->timer[state->timer_idx].full_rounds, 1, 100);
break;
case interval_setting_10_cooldown_minutes:
_inc_uint8(&state->timer[state->timer_idx].cooldown_minutes, 1, 60);
break;
case interval_setting_11_cooldown_seconds:
_inc_uint8(&state->timer[state->timer_idx].cooldown_seconds, 5, 60);
break;
default:
break;
}
}
static void _set_next_timestamp(interval_face_state_t *state) {
// set next timestamp for the running timer, set background task and pay sound sequence
uint16_t delta = 0;
int8_t *sound_seq;
interval_timer_setting_t timer = state->timer[state->timer_idx];
switch (_timer_run_state) {
case 0:
delta = timer.warmup_minutes * 60 + timer.warmup_seconds;
sound_seq = (int8_t *)_sound_seq_warmup;
break;
case 1:
delta = timer.work_minutes * 60 + timer.work_seconds;
sound_seq = (int8_t *)_sound_seq_work;
break;
case 2:
delta = timer.break_minutes * 60 + timer.break_seconds;
sound_seq = (int8_t *)_sound_seq_break;
break;
case 3:
delta = timer.cooldown_minutes * 60 + timer.cooldown_seconds;
sound_seq = (int8_t *)_sound_seq_cooldown;
break;
default:
sound_seq = NULL;
break;
}
// failsafe
if (delta <= 0) delta = 1;
_target_ts += delta;
// schedule next background task
watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(_target_ts, 0);
movement_schedule_background_task_for_face(state->face_idx, target_dt);
// play sound
watch_buzzer_play_sequence(sound_seq, NULL);
}
static inline bool _is_timer_empty(interval_timer_setting_t *timer) {
// checks if a timer is empty
return (timer->warmup_minutes + timer->warmup_seconds
+ timer->work_minutes + timer->work_seconds
+ timer->break_minutes + timer->break_seconds
+ timer->cooldown_minutes + timer->cooldown_seconds == 0);
}
static void _init_timer_info(interval_face_state_t *state) {
state->face_state = interval_state_waiting;
_ticks = 0;
if (state->is_active) movement_request_tick_frequency(2);
}
static void _abort_running_timer() {
_timer_work_round = _timer_full_round = 0;
_timer_run_state = 0;
movement_cancel_background_task();
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_buzzer_play_note(BUZZER_NOTE_C8, 100);
}
static void _resume_paused_timer(interval_face_state_t *state) {
// resume paused timer
_now_ts = _get_now_ts();
_target_ts += _now_ts - _paused_ts;
watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(_target_ts, 0);
movement_schedule_background_task_for_face(state->face_idx, target_dt);
state->face_state = interval_state_running;
watch_set_indicator(WATCH_INDICATOR_BELL);
}
void interval_face_setup(uint8_t watch_face_index, void **context_ptr) {
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(interval_face_state_t));
interval_face_state_t *state = (interval_face_state_t *)*context_ptr;
memset(*context_ptr, 0, sizeof(interval_face_state_t));
state->face_idx = watch_face_index;
// somehow the memset above doesn't to the trick. So set the state explicitly
state->face_state = interval_state_waiting;
for (uint8_t i = 0; i < INTERVAL_TIMERS; i++) state->timer[i].work_rounds = 1;
// set up default timers
for (uint8_t i = 0; i < 6; i++) {
state->timer[i].warmup_seconds = _default_timers[i][0];
if (_default_timers[i][1] < 0) state->timer[i].work_minutes = -_default_timers[i][1];
else state->timer[i].work_seconds = _default_timers[i][1];
state->timer[i].work_rounds = 1;
if (_default_timers[i][2] < 0) state->timer[i].break_minutes = -_default_timers[i][2];
else state->timer[i].break_seconds = _default_timers[i][2];
state->timer[i].full_rounds = _default_timers[i][3];
state->timer[i].cooldown_seconds = _default_timers[i][4];
}
}
}
void interval_face_activate(void *context) {
interval_face_state_t *state = (interval_face_state_t *)context;
_erase_timer_flag = false;
state->is_active = true;
if (state->face_state <= interval_state_waiting) {
// initiate the intro loop
state->face_state = interval_state_intro;
_ticks = 0;
movement_request_tick_frequency(8);
} else watch_set_colon();
}
void interval_face_resign(void *context) {
interval_face_state_t *state = (interval_face_state_t *)context;
if (state->face_state <= interval_state_setting) state->face_state = interval_state_waiting;
watch_set_led_off();
movement_request_tick_frequency(1);
state->is_active = false;
}
bool interval_face_loop(movement_event_t event, void *context) {
interval_face_state_t *state = (interval_face_state_t *)context;
interval_timer_setting_t *timer = &state->timer[state->timer_idx];
switch (event.event_type) {
case EVENT_TICK:
if (state->face_state == interval_state_intro) {
// play intro animation so the wearer knows the face
if (_ticks == 4) {
// transition to default view of current interval slot
watch_set_colon();
_init_timer_info(state);
_face_draw(state, event.subsecond);
break;
}
watch_set_pixel(_intro_segdata[_ticks][0], _intro_segdata[_ticks][1]);
_ticks++;
} else if (state->face_state == interval_state_waiting && _ticks >= 0) {
// play information slideshow for current interval timer
_ticks++;
if ((_ticks % 12 == 9) && (timer->cooldown_minutes + timer->cooldown_seconds == 0)) _ticks += 3;
if (_ticks > 24) _ticks = -1;
else _face_draw(state, event.subsecond);
} else if (state->face_state == interval_state_setting) {
if (_ticks == -2) {
// fast counting
_handle_alarm_button(state);
}
_face_draw(state, event.subsecond);
} else if (state->face_state == interval_state_running || state->face_state == interval_state_pausing) {
_now_ts = _get_now_ts();
_face_draw(state, event.subsecond);
}
break;
case EVENT_ACTIVATE:
watch_display_string(INTERVAL_FACE_STATE_DEFAULT, 0);
if (state->face_state) _face_draw(state, event.subsecond);
break;
case EVENT_LIGHT_BUTTON_UP:
if (state->face_state == interval_state_setting) {
if (_setting_idx == interval_setting_0_timer_idx) {
// skip clear page if timer is empty
if (_is_timer_empty(timer)) _setting_idx = interval_setting_1_clear_yn;
} else if (_setting_idx == interval_setting_1_clear_yn) {
watch_set_colon();
if (_erase_timer_flag) {
// clear the current timer
memset((void *)timer, 0, sizeof(interval_timer_setting_t));
// play a short beep as confirmation
watch_buzzer_play_note(BUZZER_NOTE_C8, 70);
}
} else if (_setting_idx == interval_setting_9_full_rounds && !timer->full_rounds) {
// skip cooldown if full rounds are not limited
_setting_idx = interval_setting_11_cooldown_seconds;
}
_setting_idx += 1;
if (_setting_idx == interval_setting_max) {
// we have done a full settings circle: resume setting
_resume_setting(state, event.subsecond);
} else
_face_draw(state, event.subsecond);
} else {
movement_illuminate_led();
}
break;
case EVENT_LIGHT_LONG_PRESS:
_button_beep(settings);
if (state->face_state == interval_state_setting) {
_resume_setting(state, event.subsecond);
} else {
if (state->face_state >= interval_state_running ) _abort_running_timer();
_initiate_setting(state, event.subsecond);
}
break;
case EVENT_ALARM_BUTTON_UP:
switch (state->face_state) {
case interval_state_waiting:
// cycle through timers
_inc_uint8(&state->timer_idx, 1, INTERVAL_TIMERS);
_ticks = 0;
_face_draw(state, event.subsecond);
break;
case interval_state_setting:
// alter timer settings
_abort_quick_ticks();
_handle_alarm_button(state);
break;
case interval_state_running:
// pause timer
_button_beep(settings);
_paused_ts = _get_now_ts();
state->face_state = interval_state_pausing;
movement_cancel_background_task();
_face_draw(state, event.subsecond);
break;
case interval_state_pausing:
// resume paused timer
_button_beep(settings);
_resume_paused_timer(state);
_face_draw(state, event.subsecond);
break;
default:
break;
}
break;
case EVENT_ALARM_LONG_PRESS:
if (state->face_state == interval_state_setting && _setting_idx != interval_setting_1_clear_yn) {
// initiate quick counting
_ticks = -2;
movement_request_tick_frequency(8);
break;
} else if (state->face_state <= interval_state_waiting) {
if (_is_timer_empty(timer)) {
// jump back to timer #1
_button_beep(settings);
state->timer_idx = 0;
_init_timer_info(state);
} else {
// set initial state and start timer
_timer_work_round = _timer_full_round = 0;
if (timer->warmup_minutes + timer->warmup_seconds) _timer_run_state = 0;
else if (timer->work_minutes + timer->work_seconds) _timer_run_state = 1;
else if (timer->break_minutes + timer->break_seconds) _timer_run_state = 2;
else if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3;
movement_request_tick_frequency(1);
_now_ts = _get_now_ts();
_target_ts = _now_ts;
_set_next_timestamp(state);
state->face_state = interval_state_running;
watch_set_indicator(WATCH_INDICATOR_BELL);
watch_set_colon();
}
} else if (state->face_state == interval_state_running) {
// stop the timer
_abort_running_timer();
_init_timer_info(state);
} else if (state->face_state == interval_state_pausing) {
// resume paused timer
_button_beep(settings);
_resume_paused_timer(state);
}
_face_draw(state, event.subsecond);
break;
case EVENT_ALARM_LONG_UP:
_abort_quick_ticks();
break;
case EVENT_BACKGROUND_TASK:
// find the next timestamp or end the timer
if (_timer_run_state == 0) {
// warmup finished
if (timer->work_minutes + timer->work_seconds) _timer_run_state = 1;
else if (timer->break_minutes + timer->break_seconds) _timer_run_state = 2;
else if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3;
else _timer_run_state = 4;
} else if (_timer_run_state == 1) {
// work finished
_timer_work_round++;
if (_timer_work_round == timer->work_rounds) {
_timer_work_round = 0;
if (timer->break_minutes + timer->break_seconds && (timer->full_rounds == 0
|| (timer->full_rounds && _timer_full_round + 1 < timer->full_rounds))) _timer_run_state = 2;
else {
_timer_full_round++;
if (timer->full_rounds && _timer_full_round == timer->full_rounds) {
if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3;
else _timer_run_state = 4;
} else _timer_run_state = 1;
}
}
} else if (_timer_run_state == 2) {
// break finished
_timer_full_round++;
_timer_work_round = 0;
if (timer->full_rounds && _timer_full_round == timer->full_rounds) {
if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3;
else _timer_run_state = 4;
_timer_full_round--;
} else {
if (timer->work_minutes + timer->work_seconds) _timer_run_state = 1;
}
} else if (_timer_run_state == 3)
// cooldown finished
_timer_run_state = 4;
// set next timestamp or play final sound sequence
if (_timer_run_state < 4) {
// transition to next timer phase
_set_next_timestamp(state);
} else {
// timer has finished
state->face_state = interval_state_waiting;
_init_timer_info(state);
_face_draw(state, event.subsecond);
watch_buzzer_play_sequence((int8_t *)_sound_seq_finish, NULL);
}
break;
case EVENT_TIMEOUT:
if (state->face_state != interval_state_running) movement_move_to_face(0);
break;
case EVENT_LIGHT_BUTTON_DOWN:
// don't light up every time light is hit
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}

View File

@@ -0,0 +1,127 @@
/*
* MIT License
*
* Copyright (c) 2022 Andreas Nebinger
*
* 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 INTERVAL_FACE_H_
#define INTERVAL_FACE_H_
/*
* INTERVAL TIMER face
*
* This face brings 9 customizable interval timers to the sensor watch,
* to be used as hiit training device and/or for time management techniques.
*
* - There are 9 interval timer slots, you can cycle through these with the
* alarm button (short press). For each timer slot, a short "slideshow"
* displaying the relevant details (like length of each phase - see below)
* is shown.
*
* - To start an interval timer, press and hold the alarm button.
*
* - To pause a running timer, press the alarm button (short press).
*
* - To completely abort a running timer, press and hold the alarm button.
*
* - Press and hold the light button to enter settings mode for each interval
* timer slot.
*
* - Each interval timer has 1 to 4 phases of customizable length like so:
* (1) prepare/warum up --> (2) work --> (3) break --> (4) cool down.
* When setting up or running a timer, each of these phases is displayed by
* the letters "PR" (prepare), "WO" (work), "BR" (break), "CD" (cool down).
*
* - Each of these phases is optional, you can set the corresponding
* minutes and seconds to zero. But at least one phase needs to be set, if
* you want to use the timer.
*
* - You can define the number of rounds either only for the work
* phase and/or for the combination of work + break phase. Let's say you
* want an interval timer that counts 3 rounds of 30 seconds work,
* followed by 20 seconds rest:
* work 30s --> work 30s --> work 30s --> break 20s
* You can do this by setting 30s for the "WO"rk phase and setting a 3
* in the lower right hand corner of the work page. The "LAP" indicator
* lights up at this position, to explain that we are setting laps here.
* After that, set the "BR"eak phase to 20s and leave the rest as it is.
*
* - If you want to set up a certain number of "full rounds", consisting
* of work phase(s) plus breaks, you can do so at the "BR"eak page. The
* number in the lower right hand corner determines the number of full
* rounds to be counted. A "-" means, that there is no limit and the
* timer keeps alternating between work and break phases.
*
* - This watch face comes with several pre-defined interval timers,
* suitable for hiit training (timer slots 1 to 4) as well as doing
* work according to the pomodoro principle (timer slots 5 to 6).
* Feel free to adjust the timer slots to your own needs (or completely
* wipe them ;-)
*/
#include "movement.h"
#define INTERVAL_TIMERS 9 // no of available customizable timers (be aware: only 4 bits reserved for this value in struct below)
typedef struct {
uint8_t warmup_minutes;
uint8_t warmup_seconds;
uint8_t work_minutes;
uint8_t work_seconds;
uint8_t break_minutes;
uint8_t break_seconds;
uint8_t cooldown_minutes;
uint8_t cooldown_seconds;
uint8_t work_rounds;
uint8_t full_rounds;
} interval_timer_setting_t;
typedef enum {
interval_state_intro,
interval_state_waiting,
interval_state_setting,
interval_state_running,
interval_state_pausing
} interval_timer_state_t;
typedef struct {
bool is_active;
uint8_t face_idx;
uint8_t timer_idx;
uint8_t timer_running_idx;
interval_timer_state_t face_state;
interval_timer_setting_t timer[INTERVAL_TIMERS];
} interval_face_state_t;
void interval_face_setup(uint8_t watch_face_index, void ** context_ptr);
void interval_face_activate(void *context);
bool interval_face_loop(movement_event_t event, void *context);
void interval_face_resign(void *context);
#define interval_face ((const watch_face_t) { \
interval_face_setup, \
interval_face_activate, \
interval_face_loop, \
interval_face_resign, \
NULL \
})
#endif // INTERVAL_FACE_H_

View File

@@ -0,0 +1,431 @@
/*
* MIT License
*
* Copyright (c) 2023 Andreas Nebinger
*
* 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 "watch_private_display.h"
#include "invaders_face.h"
#define INVADERS_FACE_WAVES_PER_STAGE 9 // number of waves per stage (there are two stages)
#define INVADERS_FACE_WAVE_INVADERS 16 // number of invaders attacking per wave
static const uint8_t _defense_lines_segdata[3][2] = {{2, 12}, {2, 11}, {0, 11}};
static const uint8_t _bonus_points_segdata[4][2] = {{2, 7}, {2, 8}, {2, 9}, {0, 10}};
static const uint8_t _bonus_points_helper[] = {1, 5, 9, 11, 15, 19, 21, 25, 29};
static const int8_t _sound_seq_game_start[] = {BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 3, -2, 1, BUZZER_NOTE_REST, 10, BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 3, -2, 1, 0};
static const int8_t _sound_seq_shot_hit[] = {BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 2, 0};
static const int8_t _sound_seq_shot_miss[] = {BUZZER_NOTE_A7, 1, 0};
static const int8_t _sound_seq_ufo_hit[] = {BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 2, -2, 1, 0};
static const int8_t _sound_seq_def_gone[] = {BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 3, -2, 3, BUZZER_NOTE_REST, 40, BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 3, -2, 4, 0};
static const int8_t _sound_seq_next_wave[] = {BUZZER_NOTE_A6, 2, BUZZER_NOTE_A7, 2, BUZZER_NOTE_REST, 8, BUZZER_NOTE_A6, 2, BUZZER_NOTE_A7, 2, -2, 1,
BUZZER_NOTE_REST, 32,
BUZZER_NOTE_A6, 2, BUZZER_NOTE_A7, 2, BUZZER_NOTE_REST, 8, BUZZER_NOTE_A6, 2, BUZZER_NOTE_A7, 2, -2, 1, 0};
static const int8_t _sound_seq_game_over[] = {BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 3, -2, 11, 0};
typedef enum {
invaders_state_activated,
invaders_state_pre_game,
invaders_state_playing,
invaders_state_in_wave_break,
invaders_state_pre_next_wave,
invaders_state_next_wave,
invaders_state_game_over
} invaders_current_state_t;
typedef struct {
bool ufo_next : 1; // indicates whether next invader is a ufo
bool inv_checking : 1; // flag to indicate whether we are currently moving invaders (to prevent race conditions)
bool suspend_buttons : 1; // used while playing the game over sequence to prevent involuntary immediate restarts
} invaders_signals_t;
static int8_t _invaders[6]; // array of current invaders values (-1 = empty, 10 = ufo)
static uint8_t _wave_invaders[INVADERS_FACE_WAVE_INVADERS]; // all invaders for the current wave. (Predefined to save cpu cycles when playing.)
static invaders_current_state_t _current_state;
static uint8_t _defense_lines; // number of defense lines which have been broken in the current wave
static uint8_t _aim; // current "aim" digit
static uint8_t _invader_idx; // index of next invader attacking in current wave (0 to 15)
static uint8_t _wave_position; // current position of first invader. When > 6 the defense is broken
static uint8_t _wave_tick_freq; // number of ticks passing until the next invader is inserted
static uint8_t _ticks; // counts the ticks
static uint8_t _bonus_countdown; // ticks countdown until the bonus point indicator is cleared
static uint8_t _waves; // counts the waves (_wave_tick_freq decreases slowly depending on _wave value)
static uint8_t _shots_in_wave; // number of shots in current wave. If 30 is reached, the game is over
static uint8_t _invaders_shot; // number of sucessfully shot invaders in current wave
static uint8_t _invaders_shot_sum; // current sum of invader digits shot (needed to determine if a ufo is coming)
static invaders_signals_t _signals; // holds severals flags
static uint16_t _score; // score of the current game
/// @brief return a random number. 0 <= return_value < num_values
static inline uint8_t _get_rand_num(uint8_t num_values) {
#if __EMSCRIPTEN__
return rand() % num_values;
#else
return arc4random_uniform(num_values);
#endif
}
/// @brief callback function to re-enable light and alarm buttons after playing a sound sequence
static inline void _resume_buttons() {
_signals.suspend_buttons = false;
}
/// @brief play a sound sequence if the game is in beepy mode
static inline void _play_sequence(invaders_state_t *state, int8_t *sequence) {
if (state->sound_on) watch_buzzer_play_sequence((int8_t *)sequence, NULL);
}
/// @brief draw the remaining defense lines
static void _display_defense_lines() {
watch_display_character(' ', 1);
for (uint8_t i = 0; i < 3 - _defense_lines; i++) watch_set_pixel(_defense_lines_segdata[i][0], _defense_lines_segdata[i][1]);
}
/** @brief draw label followed by the given score value
* @param label string displayed in the upper left corner
* @param score score to display
*/
static void _display_score(char *label, uint16_t score) {
watch_display_character(label[0], 0);
watch_display_character(label[1], 1);
char buf[10];
sprintf(buf, " %06d", (score * 10));
watch_display_string(buf, 2);
}
/// @brief draw an invader at the given position
static inline void _display_invader(int8_t invader, uint8_t position) {
switch (invader) {
case 10:
watch_display_character('n', position);
break;
case -1:
watch_display_character(' ', position);
break;
default:
watch_display_character(invader + 48, position);
break;
}
}
/// @brief game over: show score and set state
static void _game_over(invaders_state_t *state) {
_display_score("GO", _score);
_current_state = invaders_state_game_over;
movement_request_tick_frequency(1);
_signals.suspend_buttons = true;
if (state->sound_on) watch_buzzer_play_sequence((int8_t *)_sound_seq_game_over, _resume_buttons);
// save current score to highscore, if applicable
if (_score > state->highscore) state->highscore = _score;
}
/// @brief initialize the current wave
static void _init_wave() {
uint8_t i;
if (_current_state == invaders_state_in_wave_break) {
_invader_idx = _invaders_shot;
} else {
_invader_idx = _invaders_shot = _invaders_shot_sum = _defense_lines = _shots_in_wave = 0;
}
// pre-fill invaders
for (i = _invader_idx; i < INVADERS_FACE_WAVE_INVADERS; i++) _wave_invaders[i] = _get_rand_num(10);
// init invaders field
for (i = 1; i < 6; i++) _invaders[i] = -1;
_invaders[0] = _wave_invaders[_invader_idx];
_wave_position = _aim = _bonus_countdown = 0;
_signals.ufo_next = _signals.inv_checking = _signals.suspend_buttons = false;
_current_state = invaders_state_playing;
// determine wave speed
_wave_tick_freq = 6 - ((_waves % INVADERS_FACE_WAVES_PER_STAGE) + 1) / 2;
if (_waves >= INVADERS_FACE_WAVES_PER_STAGE) _wave_tick_freq--;
// clear display
watch_display_string(" ", 2);
watch_display_character('0', 0);
_display_defense_lines();
// draw first invader
watch_display_character(_wave_invaders[_invader_idx] + 48, 9);
}
/** @brief move invaders and add a new one, if necessary
* @returns true, if invaders have reached position 6, false otherwise
*/
static bool _move_invaders() {
if (_wave_position == 5) return true;
_signals.inv_checking = true;
if (_invaders[_wave_position] >= 0) _wave_position++;
int8_t i;
// move invaders
for (i = _wave_position; i > 0; i--) _invaders[i] = _invaders[i - 1];
if (_invader_idx < INVADERS_FACE_WAVE_INVADERS - 1) {
// add invader
_invader_idx++;
if (_signals.ufo_next) {
_invaders[0] = 10;
_signals.ufo_next = false;
} else {
_invaders[0] = _wave_invaders[_invader_idx];
}
} else {
// just add an empty invader slot
_invaders[0] = -1;
}
// update display
for (i = 0; i <= _wave_position; i++) {
_display_invader(_invaders[i], 9 - i);
}
_signals.inv_checking = false;
return false;
}
void invaders_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(invaders_state_t));
memset(*context_ptr, 0, sizeof(invaders_state_t));
invaders_state_t *state = (invaders_state_t *)*context_ptr;
// default: sound on
state->sound_on = true;
}
#if __EMSCRIPTEN__
// simulator only: seed the randon number generator
time_t t;
srand((unsigned) time(&t));
#endif
}
void invaders_face_activate(void *context) {
(void) context;
_current_state = invaders_state_activated;
_signals.suspend_buttons = false;
}
bool invaders_face_loop(movement_event_t event, void *context) {
invaders_state_t *state = (invaders_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
// show highscore
_display_score("GA", state->highscore);
break;
case EVENT_TICK:
_ticks++;
switch (_current_state) {
case invaders_state_in_wave_break:
case invaders_state_pre_game:
case invaders_state_next_wave:
// wait 2 secs to start the first round
if (_ticks >= 2) {
_ticks = 0;
_init_wave();
_current_state = invaders_state_playing;
movement_request_tick_frequency(4);
}
break;
case invaders_state_playing:
// game is playing
if (_ticks >= _wave_tick_freq) {
_ticks = 0;
if (_move_invaders()) {
// invaders broke through
if (_defense_lines < 2) {
// start current wave over
_defense_lines++;
_display_defense_lines();
_display_score("GA", _score);
_current_state = invaders_state_in_wave_break;
movement_request_tick_frequency(1);
_play_sequence(state, (int8_t *)_sound_seq_def_gone);
} else {
// game over
_game_over(state);
}
}
}
// handle bonus points indicators
if (_bonus_countdown) {
_bonus_countdown--;
if (!_bonus_countdown) {
watch_display_character(' ', 2);
watch_display_character(' ', 3);
}
}
break;
case invaders_state_pre_next_wave:
if (_ticks >= 3) {
// switch to next wave
_ticks = 0;
movement_request_tick_frequency(1);
_display_score("GA", _score);
watch_set_pixel(1, 9);
watch_display_character((_waves % INVADERS_FACE_WAVES_PER_STAGE) + 49, 3);
_current_state = invaders_state_next_wave;
_waves++;
if (_waves == INVADERS_FACE_WAVES_PER_STAGE * 2) _waves = 0;
_play_sequence(state, (int8_t *)_sound_seq_next_wave);
}
default:
break;
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
if (!_signals.suspend_buttons) {
if (_current_state == invaders_state_playing) {
// cycle the aim
_aim = (_aim + 1) % 11;
_display_invader(_aim, 0);
} else if (_current_state == invaders_state_activated || _current_state == invaders_state_game_over) {
// just illuminate the LED
movement_illuminate_led();
}
}
break;
case EVENT_LIGHT_LONG_PRESS:
if ((_current_state == invaders_state_activated || _current_state == invaders_state_game_over) && !_signals.suspend_buttons) {
// switch between beepy and silent mode
state->sound_on = !state->sound_on;
watch_buzzer_play_note(BUZZER_NOTE_A7, state->sound_on ? 65 : 25);
}
break;
case EVENT_ALARM_BUTTON_DOWN:
if (!_signals.suspend_buttons) {
switch (_current_state) {
case invaders_state_game_over:
case invaders_state_activated:
// initialize the game
_waves = 0;
_score = 0;
movement_request_tick_frequency(1);
_ticks = 0;
_current_state = invaders_state_pre_game;
_play_sequence(state, (int8_t *)_sound_seq_game_start);
break;
case invaders_state_playing: {
// "shoot"
_shots_in_wave++;
if (_shots_in_wave == 30) {
// max number of shots reached: game over
_game_over(state);
} else {
// wait if we are currently deleting an invader
while (_signals.inv_checking);
// proceed
_signals.inv_checking = true;
bool skip = false;
for (int8_t i = _wave_position; i >= 0 && !skip; i--) {
// if (_invaders[i] == -1) break;
if (_invaders[i] == _aim) {
// invader is shot
skip = true;
_invaders_shot++;
_play_sequence(state, _aim == 10 ? (int8_t *)_sound_seq_ufo_hit : (int8_t *)_sound_seq_shot_hit);
if (_invaders_shot == INVADERS_FACE_WAVE_INVADERS) {
// last invader shot: wave sucessfully completed
watch_display_character(' ', 9 - _wave_position);
_ticks = 0;
_current_state = invaders_state_pre_next_wave;
_signals.inv_checking = false;
} else {
// check for ufo appearance
if (_aim && _aim < 10) {
_invaders_shot_sum = (_invaders_shot_sum + _aim) % 10;
if (_invaders_shot_sum == 0) _signals.ufo_next = true;
}
// remove invader
if (_wave_position == 0 || i == 5) {
_invaders[i] = -1;
} else {
for (uint8_t j = i; j < _wave_position; j++) {
_invaders[j] = _invaders[j + 1];
_display_invader(_invaders[j], 9 - j);
}
}
watch_display_character(' ', 9 - _wave_position);
if (_wave_position) _wave_position--;
// update score
if (_aim == 10) {
// ufo shot. The original game uses a ridiculously complicated scoring system here...
uint8_t bonus_points = 0;
uint8_t j;
for (j = 0; j < sizeof(_bonus_points_helper) && !bonus_points; j++) {
if (_shots_in_wave == _bonus_points_helper[j]) {
bonus_points = 30;
} else if (_shots_in_wave - 1 == _bonus_points_helper[j]) {
bonus_points = 20;
}
}
if (!bonus_points) bonus_points = 10;
bonus_points += (6 - i);
if ((_waves >= INVADERS_FACE_WAVES_PER_STAGE) && i) bonus_points += (6 - i);
_score += bonus_points;
// represent bonus points by bars
for (j = 0; j < (bonus_points / 10); j++) watch_set_pixel(_bonus_points_segdata[j][0], _bonus_points_segdata[j][1]);
_bonus_countdown = 9;
} else {
// regular invader
_score += (6 - _wave_position) * (_waves >= INVADERS_FACE_WAVES_PER_STAGE ? 2 : 1);
}
}
}
}
if (!skip) _play_sequence(state, (int8_t *)_sound_seq_shot_miss);
_signals.inv_checking = false;
}
break;
}
default:
break;
}
}
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
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);
}
// return true if the watch can enter standby mode. Generally speaking, you should always return true.
// Exceptions:
// * If you are displaying a color using the low-level watch_set_led_color function, you should return false.
// * If you are sounding the buzzer using the low-level watch_set_buzzer_on function, you should return false.
// Note that if you are driving the LED or buzzer using Movement functions like movement_illuminate_led or
// movement_play_alarm, you can still return true. This guidance only applies to the low-level watch_ functions.
return true;
}
void invaders_face_resign(void *context) {
(void) context;
_current_state = invaders_state_game_over;
}

View File

@@ -0,0 +1,82 @@
/*
* MIT License
*
* Copyright (c) 2023 Andreas Nebinger
*
* 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 INVADERS_FACE_H_
#define INVADERS_FACE_H_
/*
* Remake of the "famous" Casio Number Invaders Game
*
* This is an authentic remake of the invaders game, found on the Casio
* calculator wristwatch CA-85 or CA-851. There were also some calculators
* sold with this game, like MG-880.
*
* How to play:
*
* Press the alarm button to start the game.
* "Invaders" (just digits, tbh) will start coming in from the right hand side.
* Press the light button to "aim". The digit on the top of the display cycles
* from 0 to 9. If your aiming digit is identical to one of the invaders,
* press the alarm button to "shoot". The corresponding invader will disappear.
* If the invaders reach beneath the very first position, you loose one defense
* line. When all three defense lines are gone, the game is over.
* Also: If you shoot more than 29 times per round, you loose the game.
* Good to know: There are 16 invaders per wave. There is a short break between
* waves.
*
* What are the "n" invaders? Ufos!
*
* Whenever the sum of all invaders shot is divisible by 10 the next invader
* will be an ufo, represented by the n-symbol. Shooting a ufo gets you extra
* points. Example: shoot 2, 5, 3 --> ufo next
*
* As for points: the earlier you shoot an invader, the more points you get.
*
* Anything else? Long pressing the light button toggles sound on or off. (Not
* while playing.)
*
*/
#include "movement.h"
typedef struct {
uint16_t highscore;
bool sound_on;
} invaders_state_t;
void invaders_face_setup(uint8_t watch_face_index, void ** context_ptr);
void invaders_face_activate(void *context);
bool invaders_face_loop(movement_event_t event, void *context);
void invaders_face_resign(void *context);
#define invaders_face ((const watch_face_t){ \
invaders_face_setup, \
invaders_face_activate, \
invaders_face_loop, \
invaders_face_resign, \
NULL, \
})
#endif // INVADERS_FACE_H_

View File

@@ -0,0 +1,477 @@
/*
* MIT License
*
* Copyright (c) 2023 PrimmR
*
* 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 "kitchen_conversions_face.h"
typedef struct
{
char name[6]; // Name to display on selection
double conv_factor_uk; // Unit as represented in base units (UK)
double conv_factor_us; // Unit as represented in base units (US)
int16_t linear_factor; // Addition of constant (For temperatures)
} unit;
#define TICK_FREQ 4
#define MEASURES_COUNT 3 // Number of different measurement 'types'
#define WEIGHT 0
#define TEMP 1
#define VOL 2
// Names of measurements
static char measures[MEASURES_COUNT][6] = {"WeIght", " Temp", " VOL"};
// Number of items in each category
#define WEIGHT_COUNT 4
#define TEMP_COUNT 3
#define VOL_COUNT 9
const uint8_t units_count[4] = {WEIGHT_COUNT, TEMP_COUNT, VOL_COUNT};
static const unit weights[WEIGHT_COUNT] = {
{" g", 1., 1., 0}, // BASE
{" kg", 1000., 1000, 0},
{"Ounce", 28.34952, 28.34952, 0},
{" Pound", 453.5924, 453.5924, 0},
};
static const unit temps[TEMP_COUNT] = {
{" # C", 1.8, 1.8, 32},
{" # F", 1., 1., 0}, // BASE
{"Gas Mk", 25., 25., 250},
};
static const unit vols[VOL_COUNT] = {
{" n&L", 1., 1., 0}, // BASE (ml)
{" L", 1000., 1000., 0},
{" Fl Oz", 28.41306, 29.57353, 0},
{" Tbsp", 17.75816, 14.78677, 0},
{" Tsp", 5.919388, 4.928922, 0},
{" Cup", 284.1306, 236.5882, 0},
{" Pint", 568.2612, 473.1765, 0},
{" Quart", 1136.522, 946.353, 0},
{"Gallon", 4546.09, 3785.412, 0},
};
static int8_t calc_success_seq[5] = {BUZZER_NOTE_G6, 10, BUZZER_NOTE_C7, 10, 0};
static int8_t calc_fail_seq[5] = {BUZZER_NOTE_C7, 10, BUZZER_NOTE_G6, 10, 0};
// Resets all state variables to 0
static void reset_state(kitchen_conversions_state_t *state)
{
state->pg = measurement;
state->measurement_i = 0;
state->from_i = 0;
state->from_is_us = movement_use_imperial_units(); // If uses imperial, most likely to be US
state->to_i = 0;
state->to_is_us = movement_use_imperial_units();
state->selection_value = 0;
state->selection_index = 0;
state->light_held = false;
}
void kitchen_conversions_face_setup(uint8_t watch_face_index, void **context_ptr)
{
(void)watch_face_index;
if (*context_ptr == NULL)
{
*context_ptr = malloc(sizeof(kitchen_conversions_state_t));
memset(*context_ptr, 0, sizeof(kitchen_conversions_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.
}
void kitchen_conversions_face_activate(void *context)
{
kitchen_conversions_state_t *state = (kitchen_conversions_state_t *)context;
// Handle any tasks related to your watch face coming on screen.
movement_request_tick_frequency(TICK_FREQ);
reset_state(state, settings);
}
// Increments index pointer by 1, wrapping
#define increment_wrapping(index, wrap) ({(index)++; index %= wrap; })
static uint32_t pow_10(uint8_t n)
{
uint32_t result = 1;
for (int i = 0; i < n; i++)
{
result *= 10;
}
return result;
}
// Returns correct list of units for the measurement index
static unit *get_unit_list(uint8_t measurement_i)
{
switch (measurement_i)
{
case WEIGHT:
return (unit *)weights;
case TEMP:
return (unit *)temps;
case VOL:
return (unit *)vols;
default:
return (unit *)weights;
}
}
// Increment digit by 1 in input (wraps)
static void increment_input(kitchen_conversions_state_t *state)
{
uint8_t digit = state->selection_value / pow_10(DISPLAY_DIGITS - 1 - state->selection_index) % 10;
if (digit != 9)
{
state->selection_value += pow_10(DISPLAY_DIGITS - 1 - state->selection_index);
}
else
{
state->selection_value -= 9 * pow_10(DISPLAY_DIGITS - 1 - state->selection_index);
}
}
// Displays the list of units in the selected category
static void display_units(uint8_t measurement_i, uint8_t list_i)
{
watch_display_string(get_unit_list(measurement_i)[list_i].name, 4);
}
static void display(kitchen_conversions_state_t *state, uint8_t subsec)
{
watch_clear_display();
switch (state->pg)
{
case measurement:
{
watch_display_string("Un", 0);
watch_display_string(measures[state->measurement_i], 4);
}
break;
case from:
display_units(state->measurement_i, state->from_i);
// Display Fr if non-locale specific, else display locale and F
if (state->measurement_i == VOL)
{
watch_display_string("F", 3);
char *locale = state->from_is_us ? "A " : "GB";
watch_display_string(locale, 0);
}
else
{
watch_display_string("Fr", 0);
}
break;
case to:
display_units(state->measurement_i, state->to_i);
// Display To if non-locale specific, else display locale and T
if (state->measurement_i == VOL)
{
watch_display_string("T", 3);
char *locale = state->to_is_us ? "A " : "GB";
watch_display_string(locale, 0);
}
else
{
watch_display_string("To", 0);
}
break;
case input:
{
char buf[7];
sprintf(buf, "%06lu", state->selection_value);
watch_display_string(buf, 4);
// Only allow ints for Gas Mk
if (state->measurement_i == TEMP && state->from_i == 2)
{
watch_display_string(" ", 8);
}
// Blink digit (on & off) twice a second
if (subsec % 2)
{
watch_display_string(" ", 4 + state->selection_index);
}
watch_display_string("In", 0);
}
break;
case result:
{
unit froms = get_unit_list(state->measurement_i)[state->from_i];
unit tos = get_unit_list(state->measurement_i)[state->to_i];
// Chooses correct factor for locale
double f_conv_factor = state->from_is_us ? froms.conv_factor_us : froms.conv_factor_uk;
double t_conv_factor = state->to_is_us ? tos.conv_factor_us : tos.conv_factor_uk;
// Converts
double to_base = (state->selection_value * f_conv_factor) + 100 * froms.linear_factor;
double conversion = ((to_base - 100 * tos.linear_factor) / t_conv_factor);
// If number too large or too small
uint8_t lower_bound = (state->measurement_i == TEMP && state->to_i == 2) ? 100 : 0;
if (conversion >= 1000000 || conversion < lower_bound)
{
watch_set_indicator(WATCH_INDICATOR_BELL);
watch_display_string("Err", 5);
if (movement_button_should_sound())
watch_buzzer_play_sequence(calc_fail_seq, NULL);
}
else
{
uint32_t rounded = conversion + .5;
char buf[7];
sprintf(buf, "%6lu", rounded);
watch_display_string(buf, 4);
// Make sure LSDs always filled
if (rounded < 10)
{
watch_display_string("00", 7);
}
else if (rounded < 100)
{
watch_display_string("0", 7);
}
if (movement_button_should_sound())
watch_buzzer_play_sequence(calc_success_seq, NULL);
}
watch_display_string("=", 1);
}
break;
default:
break;
}
}
bool kitchen_conversions_face_loop(movement_event_t event, void *context)
{
kitchen_conversions_state_t *state = (kitchen_conversions_state_t *)context;
switch (event.event_type)
{
case EVENT_ACTIVATE:
// Initial UI
display(state, settings, event.subsecond);
break;
case EVENT_TICK:
// Update for blink animation on input
if (state->pg == input)
{
display(state, settings, event.subsecond);
// Increments input twice a second when light button held
if (state->light_held && event.subsecond % 2)
increment_input(state);
}
break;
case EVENT_LIGHT_BUTTON_UP:
// Cycles options
switch (state->pg)
{
case measurement:
increment_wrapping(state->measurement_i, MEASURES_COUNT);
break;
case from:
increment_wrapping(state->from_i, units_count[state->measurement_i]);
break;
case to:
increment_wrapping(state->to_i, units_count[state->measurement_i]);
break;
case input:
increment_input(state);
break;
default:
break;
}
// Light button does nothing on final screen
if (state->pg != result)
display(state, settings, event.subsecond);
state->light_held = false;
break;
case EVENT_ALARM_BUTTON_UP:
// Increments selected digit
if (state->pg == input)
{
// Moves between digits in input
// Wraps at 6 digits unless gas mark selected
if (state->selection_index < (DISPLAY_DIGITS - 1) - 2 * (state->measurement_i == TEMP && state->from_i == 2))
{
state->selection_index++;
}
else
{
state->pg++;
display(state, settings, event.subsecond);
}
}
// Moves forward 1 page
else
{
if (state->pg == SCREEN_NUM - 1)
{
reset_state(state, settings);
}
else
{
state->pg++;
}
// Play boop
if (movement_button_should_sound())
watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
}
display(state, settings, event.subsecond);
state->light_held = false;
break;
case EVENT_ALARM_LONG_PRESS:
// Moves backwards through pages, resetting certain values
if (state->pg != measurement)
{
switch (state->pg)
{
case measurement:
state->measurement_i = 0;
break;
case from:
state->from_i = 0;
state->from_is_us = movement_use_imperial_units();
break;
case to:
state->to_i = 0;
state->to_is_us = movement_use_imperial_units();
break;
case input:
state->selection_index = 0;
state->selection_value = 0;
break;
case result:
state->selection_index = 0;
break;
default:
break;
}
state->pg--;
display(state, settings, event.subsecond);
// Play beep
if (movement_button_should_sound())
watch_buzzer_play_note(BUZZER_NOTE_C8, 50);
state->light_held = false;
}
break;
case EVENT_LIGHT_LONG_PRESS:
// Switch between locales
if (state->measurement_i == VOL)
{
if (state->pg == from)
{
state->from_is_us = !state->from_is_us;
}
else if (state->pg == to)
{
state->to_is_us = !state->to_is_us;
}
if (state->pg == from || state->pg == to)
{
display(state, settings, event.subsecond);
// Play bleep
if (movement_button_should_sound())
watch_buzzer_play_note(BUZZER_NOTE_E7, 50);
}
}
// Sets flag to increment input digit when light held
if (state->pg == input)
state->light_held = true;
break;
case EVENT_LIGHT_LONG_UP:
state->light_held = false;
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void kitchen_conversions_face_resign(void *context)
{
(void)context;
// handle any cleanup before your watch face goes off-screen.
}

View File

@@ -0,0 +1,87 @@
/*
* MIT License
*
* Copyright (c) 2023 PrimmR
*
* 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 KITCHEN_CONVERSIONS_FACE_H_
#define KITCHEN_CONVERSIONS_FACE_H_
#include "movement.h"
/*
* Kitchen Conversions
* A face that allows the user to convert between common kitchen units of measurement
*
* How to use
* ----------
* Short press the alarm button to move forward through menus, and long press to move backwards
*
* Press the light button to cycle through options in the menus
*
* When inputting a number, the light button moves forward one place and the alarm button increments a digit by one
*
* To convert between Imperial (GB) and US (A) measurements of volume, hold the light button
*
*/
#define SCREEN_NUM 5
// Names of each page
typedef enum
{
measurement,
from,
to,
input,
result,
} page_t;
#define DISPLAY_DIGITS 6
// Settings when app is running
typedef struct
{
page_t pg;
uint8_t measurement_i;
uint8_t from_i;
bool from_is_us;
uint8_t to_i;
bool to_is_us;
uint32_t selection_value;
uint8_t selection_index;
bool light_held;
} kitchen_conversions_state_t;
void kitchen_conversions_face_setup(uint8_t watch_face_index, void **context_ptr);
void kitchen_conversions_face_activate(void *context);
bool kitchen_conversions_face_loop(movement_event_t event, void *context);
void kitchen_conversions_face_resign(void *context);
#define kitchen_conversions_face ((const watch_face_t){ \
kitchen_conversions_face_setup, \
kitchen_conversions_face_activate, \
kitchen_conversions_face_loop, \
kitchen_conversions_face_resign, \
NULL, \
})
#endif // KITCHEN_CONVERSIONS_FACE_H_

View File

@@ -0,0 +1,469 @@
/*
* 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() {
if (movement_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_t 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_t 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 812 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_t 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_t 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_t 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(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
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(void *context) {
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_get_current_timezone_offset();
movement_request_tick_frequency(4); // we need to manually blink some pixels
}
bool menstrual_cycle_face_loop(movement_event_t event, void *context) {
menstrual_cycle_state_t *state = (menstrual_cycle_state_t *)context;
watch_date_time_t 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_sleep_animation_is_running())
watch_stop_sleep_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);
}
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_sleep_animation_is_running())
watch_start_sleep_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(void *context) {
(void) context;
}

View 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(uint8_t watch_face_index, void ** context_ptr);
void menstrual_cycle_face_activate(void *context);
bool menstrual_cycle_face_loop(movement_event_t event, void *context);
void menstrual_cycle_face_resign(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_

View File

@@ -0,0 +1,260 @@
/*
* 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(uint8_t watch_face_index, void ** context_ptr) {
(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(void *context) {
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, 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);
}
return true;
}
void metronome_face_resign(void *context) {
(void) context;
}

View 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(uint8_t watch_face_index, void ** context_ptr);
void metronome_face_activate(void *context);
bool metronome_face_loop(movement_event_t event, void *context);
void metronome_face_resign(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_

View File

@@ -0,0 +1,179 @@
/*
* MIT License
*
* Copyright (c) 2022 Joey Castillo
*
* Based on Phase of Moon App for Tidbyt
* https://github.com/tidbyt/community/blob/main/apps/phaseofmoon/phase_of_moon.star
* Copyright (c) 2022 Alan Fleming
*
*
* 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 <stdio.h>
#include <math.h>
#include "moon_phase_face.h"
#include "watch_utility.h"
#define LUNAR_DAYS 29.53058770576
#define LUNAR_SECONDS (LUNAR_DAYS * (24 * 60 * 60))
#define FIRST_MOON 947182440 // Saturday, 6 January 2000 18:14:00 in unix epoch time
#define NUM_PHASES 8
static const float phase_changes[] = {0, 1, 6.38264692644, 8.38264692644, 13.76529385288, 15.76529385288, 21.14794077932, 23.14794077932, 28.53058770576, 29.53058770576};
void moon_phase_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(moon_phase_state_t));
memset(*context_ptr, 0, sizeof(moon_phase_state_t));
}
}
void moon_phase_face_activate(void *context) {
(void) context;
}
static void _update(moon_phase_state_t *state, uint32_t offset) {
(void)state;
char buf[11];
watch_date_time_t date_time = watch_rtc_get_date_time();
uint32_t now = watch_utility_date_time_to_unix_time(date_time, movement_get_current_timezone_offset()) + offset;
date_time = watch_utility_date_time_from_unix_time(now, movement_get_current_timezone_offset());
double currentfrac = fmod(now - FIRST_MOON, LUNAR_SECONDS) / LUNAR_SECONDS;
double currentday = currentfrac * LUNAR_DAYS;
uint8_t phase_index = 0;
for(phase_index = 0; phase_index <= NUM_PHASES; phase_index++) {
if (currentday > phase_changes[phase_index] && currentday <= phase_changes[phase_index + 1]) break;
}
watch_display_string(" ", 0);
switch (phase_index) {
case 0:
case 8:
sprintf(buf, "%2d Neu ", date_time.unit.day);
break;
case 1:
sprintf(buf, "%2dCresnt", date_time.unit.day);
watch_set_pixel(2, 13);
watch_set_pixel(2, 15);
if (currentfrac > 0.125) watch_set_pixel(1, 13);
break;
case 2:
sprintf(buf, "%2d 1st q", date_time.unit.day);
watch_set_pixel(2, 13);
watch_set_pixel(2, 15);
watch_set_pixel(1, 13);
watch_set_pixel(1, 14);
break;
case 3:
sprintf(buf, "%2d Gibb ", date_time.unit.day);
watch_set_pixel(2, 13);
watch_set_pixel(2, 15);
watch_set_pixel(1, 14);
watch_set_pixel(1, 13);
watch_set_pixel(1, 15);
break;
case 4:
sprintf(buf, "%2d FULL ", date_time.unit.day);
watch_set_pixel(2, 13);
watch_set_pixel(2, 15);
watch_set_pixel(1, 14);
watch_set_pixel(2, 14);
watch_set_pixel(1, 15);
watch_set_pixel(0, 14);
watch_set_pixel(0, 13);
watch_set_pixel(1, 13);
break;
case 5:
sprintf(buf, "%2d Gibb ", date_time.unit.day);
watch_set_pixel(1, 14);
watch_set_pixel(2, 14);
watch_set_pixel(1, 15);
watch_set_pixel(0, 14);
watch_set_pixel(0, 13);
break;
case 6:
sprintf(buf, "%2d 3rd q", date_time.unit.day);
watch_set_pixel(1, 14);
watch_set_pixel(2, 14);
watch_set_pixel(0, 14);
watch_set_pixel(0, 13);
break;
case 7:
sprintf(buf, "%2dCresnt", date_time.unit.day);
watch_set_pixel(0, 14);
watch_set_pixel(0, 13);
if (currentfrac < 0.875) watch_set_pixel(2, 14);
break;
}
watch_display_string(buf, 2);
}
bool moon_phase_face_loop(movement_event_t event, void *context) {
moon_phase_state_t *state = (moon_phase_state_t *)context;
watch_date_time_t date_time;
switch (event.event_type) {
case EVENT_ACTIVATE:
_update(state, state->offset);
break;
case EVENT_TICK:
// only update once an hour
date_time = watch_rtc_get_date_time();
if ((date_time.unit.minute == 0) && (date_time.unit.second == 0)) _update(state, state->offset);
break;
case EVENT_LOW_ENERGY_UPDATE:
// update at the top of the hour OR if we're entering sleep mode with an offset.
// also, in sleep mode, always show the current moon phase (offset = 0).
if (state->offset || (watch_rtc_get_date_time().unit.minute == 0)) _update(state, 0);
// and kill the offset so when the wearer wakes up, it matches what's on screen.
state->offset = 0;
// finally: clear out the last two digits and replace them with the sleep mode indicator
watch_display_string(" ", 8);
if (!watch_sleep_animation_is_running()) watch_start_sleep_animation(1000);
break;
case EVENT_ALARM_BUTTON_UP:
// Pressing the alarm adds an offset of one day to the displayed value,
// so you can see moon phases in the future.
state->offset += 86400;
_update(state, state->offset);
break;
case EVENT_ALARM_LONG_PRESS:
state->offset = 0;
_update(state, state->offset);
break;
case EVENT_TIMEOUT:
// QUESTION: Should timeout reset offset to 0?
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void moon_phase_face_resign(void *context) {
moon_phase_state_t *state = (moon_phase_state_t *)context;
state->offset = 0;
}

View File

@@ -0,0 +1,72 @@
/*
* 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 MOON_PHASE_FACE_H_
#define MOON_PHASE_FACE_H_
/*
* MOON PHASE face
*
* The Moon Phase face is similar to the Sunrise/Sunset face: it displays the
* current phase of the moon, along with the day of the month and a graphical
* representation of the moon on the top row.
*
* This graphical representation is a bit abstract. The segments that turn on
* represent the shape of the moon, waxing from the bottom right and waning at
* the top left. A small crescent at the bottom right will grow into a larger
* crescent, then add lines in the center for a quarter and half moon. All
* segments are on during a full moon. Then gradually the segments at the
* bottom right will turn off, until all that remains is a small waning
* crescent at the top left.
*
* All segments turn off during a new moon.
*
* On this screen you may press the Alarm button repeatedly to move forward
* in time: the day of the month at the top right will advance by one day for
* each button press, and both the text and the graphical representation will
* display the moon phase for that day. Try pressing the Alarm button 27 times
* now, just to visualize what the moon will look like over the next month.
*/
#include "movement.h"
typedef struct {
uint32_t offset;
} moon_phase_state_t;
void moon_phase_face_setup(uint8_t watch_face_index, void ** context_ptr);
void moon_phase_face_activate(void *context);
bool moon_phase_face_loop(movement_event_t event, void *context);
void moon_phase_face_resign(void *context);
#define moon_phase_face ((const watch_face_t){ \
moon_phase_face_setup, \
moon_phase_face_activate, \
moon_phase_face_loop, \
moon_phase_face_resign, \
NULL, \
})
#endif // MOON_PHASE_FACE_H_

View File

@@ -0,0 +1,204 @@
/*
* MIT License
*
* Copyright (c) 2023 Christian Chapman
*
* 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 <math.h>
#include "watch.h"
#include "watch_utility.h"
#include "watch_private_display.h"
#include "morsecalc_face.h"
#include "morsecalc_display.h"
/* mc_input Read an input into a morse code buffer
* Input: mc = index of MORSECODE_TREE[]
* len = max morse code char length
* in = character to read into buffer (0='.', 1='-', ignored otherwise).
* If the buffer is full, reset it instead of entering the new character.
*/
static void morsecode_input(unsigned int *mc, unsigned int len, char in) {
if(*mc >= (unsigned int) ((1<<len)-1)) *mc = 0;
else if((in == 0) | (in == 1)) *mc = (*mc)*2+in+1;
return;
}
// Clear token buffer
void morsecalc_reset_token(morsecalc_state_t *mcs) {
memset(mcs->token, '\0', MORSECALC_TOKEN_LEN*sizeof(mcs->token[0]));
mcs->idxt = 0;
return;
}
// Write a completed morse code character to the calculator
void morsecalc_input(morsecalc_state_t * mcs) {
int status = 0;
char dec = MORSECODE_TREE[mcs->mc];
mcs->mc = 0;
switch(dec) {
case '\0': // Invalid character, do nothing
morsecalc_display_token(mcs);
break;
case ' ': // Submit token to calculator
if(mcs->idxt > 0) {
mcs->token[mcs->idxt] = '\0';
status = calc_input(mcs->cs, mcs->token);
morsecalc_reset_token(mcs);
}
morsecalc_display_stack(mcs);
break;
case '(': // -.--. Erase previous character in token
if(mcs->idxt>0) {
mcs->idxt--;
mcs->token[mcs->idxt] = '\0';
}
morsecalc_display_token(mcs);
break;
case 'S': // -.-.- Erase entire token without submitting
morsecalc_reset_token(mcs);
morsecalc_display_stack(mcs);
break;
default: // Add character to token
if(mcs->idxt < MORSECALC_TOKEN_LEN-1) {
mcs->token[mcs->idxt] = dec;
mcs->idxt = min(mcs->idxt+1, MORSECALC_TOKEN_LEN);
morsecalc_display_token(mcs);
}
else watch_display_string(" full", 4);
break;
}
// Print errors if there are any
switch(status) {
case 0: break; // Success
case -1: watch_display_string("cmderr", 4); break; // Unrecognized command
case -2: watch_display_string("stkerr", 4); break; // Bad stack size
default: watch_display_string(" err", 4); break; // Other error
}
return;
}
void morsecalc_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(morsecalc_state_t));
morsecalc_state_t *mcs = (morsecalc_state_t *)*context_ptr;
morsecalc_reset_token(mcs);
mcs->cs = (calc_state_t *) malloc(sizeof(calc_state_t));
calc_init(mcs->cs);
mcs->mc = 0;
mcs->led_is_on = 0;
}
return;
}
void morsecalc_face_activate(void *context) {
morsecalc_state_t *mcs = (morsecalc_state_t *) context;
mcs->mc = 0;
morsecalc_display_stack(mcs);
return;
}
bool morsecalc_face_loop(movement_event_t event, void *context) {
morsecalc_state_t *mcs = (morsecalc_state_t *) context;
switch(event.event_type) {
// input
case EVENT_ALARM_BUTTON_UP:
// dot
morsecode_input(&mcs->mc, MORSECODE_LEN, 0);
morsecalc_display_token(mcs);
break;
case EVENT_LIGHT_BUTTON_UP:
// dash
morsecode_input(&mcs->mc, MORSECODE_LEN, 1);
morsecalc_display_token(mcs);
break;
case EVENT_MODE_BUTTON_UP:
// submit character (or quit)
if(mcs->mc || mcs->idxt) morsecalc_input(mcs);
else movement_move_to_next_face();
break;
// show stack
case EVENT_ALARM_LONG_PRESS:
morsecalc_display_stack(mcs);
mcs->mc = 0;
break;
// toggle light
case EVENT_LIGHT_LONG_PRESS:
mcs->led_is_on = !mcs->led_is_on;
if(mcs->led_is_on) {
movement_color_t color = movement_backlight_color();
watch_set_led_color(color.red ? (0xF | color.red << 4) : 0,
color.green ? (0xF | color.green << 4) : 0);
movement_request_tick_frequency(4);
}
else {
watch_set_led_off();
movement_request_tick_frequency(1);
}
break;
// quit
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
case EVENT_MODE_LONG_PRESS:
movement_move_to_next_face();
break;
case EVENT_TICK:
if(mcs->led_is_on) {
movement_color_t color = movement_backlight_color();
watch_set_led_color(color.red ? (0xF | color.red << 4) : 0,
color.green ? (0xF | color.green << 4) : 0);
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
// don't light up every time light is hit
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void morsecalc_face_resign(void *context) {
morsecalc_state_t *mcs = (morsecalc_state_t *) context;
mcs->led_is_on = 0;
watch_set_led_off();
return;
}

View File

@@ -0,0 +1,163 @@
/*
* MIT License
*
* Copyright (c) 2023 Christian Chapman
*
* 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 MORSECALC_FACE_H_
#define MORSECALC_FACE_H_
/*
* MORSECALC face
* Morse-code-based RPN calculator
*
* The calculator is operated by first composing a **token** in Morse code,
* then submitting it to the calculator. A token specifies either a calculator
* operation or a float value.
*
* These two parts of the codebase are totally independent:
* 1. The Morse-code reader (`mc.h`, `mc.c`)
* 2. The RPN calculator (`calc.h`, `calc.c`, `calc_fn.h`, `calc_fn.c`, `small_strtod.c`)
*
* The user interface (`morsecalc_face.h`, `morsecalc_face.c`) lets you talk
* to the RPN calculator through Morse code.
*
* ## Controls
* - `light` is dash
* - `alarm` is dot
* - `mode` is "finish character"
* - long-press `mode` or submit a blank token to switch faces
* - long-press `alarm` to show stack
* - long-press `light` to toggle the light
*
* ## Morse code token entry
* As you enter `.`s and `-`s, the morse code char you've entered will
* appear in the top center digit. At the top right is the # of morse code
* `.`/`-` you've input so far. The character resets at the 6th `.`/`-`.
*
* Once you have the character you want to enter, push `mode` to enter it.
*
* The character will be appended to the current token, whose 6 trailing
* chars are shown on the main display. Once you've typed in the token you
* want, enter a blank Morse code character and then push `mode`.
* This submits it to the calculator.
*
* Special characters:
* - Backspace is `(` (`-.--.`).
* - Clear token input without submitting to calculator is `Start
* transmission` (`-.-.-`).
*
* ## Writing commands
* First the calculator will try to interpret the token as a command/stack operation.
* Commands are defined in `calc_dict[]` in `movement/lib/morsecalc/calc_fns.h`.
* If the command doesn't appear in the dictionary, the calculator tries to interpret the token as a number.
*
* ## Writing numbers
* Numbers are written like floating point strings.
* Entering a number pushes it to the top of the stack if there's room.
* This can get long, so for convenience numerals can also be written in binary with .- = 01.
*
* 0 1 2 3 4 5 6 7 8 9
* . - -. -- -.. -.- --. --- -... -..-
* e t n m d k g o b x
*
* - Exponent signs must be entered as "p".
* - Decimal place "." can be entered as "h" (code ....)
* - Sign "-" can be entered as "Ch digraph" (code ----)
*
* For example: "4.2e-3" can be entered directly, or as "4h2pC3"
* similarly, "0.0042" can also be entered as "eheedn"
* Once you submit a number to the watch face, it pushes it to the top of the stack if there's room.
*
* ## Number display
* After a command runs, the top of the stack is displayed in this format:
*
* - Main 4 digits = leading 4 digits
* - Last 2 digits = exponent
* - Top middle = [Stack location, Sign of number]
* - Top right = [Stack exponent, Sign of exponent]
*
* Blank sign digit means positive.
* So for example, the watch face might look like this:
*
* [ 0 -5]
* [4200 03]
*
* ... representing `+4.200e-3` is in stack location 0 (the top) and it's one of five items in the stack.
*
* ## Looking at the stack
* To show the top of the stack, push and hold `light`/`alarm` or submit a blank token by pushing `mode` a bunch of times.
* To show the N-th stack item (0 through 9):
*
* - Put in the Morse code for N without pushing the mode button.
* - Push and hold `alarm`.
*
* To show the memory register, use `m` instead of a number.
*
* To see all the calculator operations and their token aliases, see the `calc_dict[]` struct in `calc_fns.h`
*/
#define MORSECALC_TOKEN_LEN 32
#define MORSECODE_LEN 5
#include "movement.h"
#include "calc.h"
/*
* MC International Morse Code binary tree
* Levels of the tree are concatenated.
* '.' = 0 and '-' = 1.
*
* Capitals denote special characters:
* C = Ch digraph
* V = VERIFY (ITU-R "UNDERSTOOD")
* R = REPEAT
* W = WAIT
* S = START TRANSMISSION
* E = END OF WORK
*/
static const char MORSECODE_TREE[] = " etianmsurwdkgohvf\0l\0pjbxcyzq\0C\x35\x34V\x33\0R\0\x32W\0+\0\0\0\0\x31\x36=/\0\0S(\0\x37\0\0\0\x38\0\x39\x30\0\0\0\0\0E\0\0\0\0\0\0?_\0\0\0\0\"\0\0.\0\0\0\0@\0\0\0'\0\0-\0\0\0\0\0\0\0\0;!\0)\0\0\0\0\0,\0\0\0\0:\0\0\0\0\0\0";
void morsecalc_face_setup(uint8_t watch_face_index, void ** context_ptr);
void morsecalc_face_activate(void *context);
bool morsecalc_face_loop(movement_event_t event, void *context);
void morsecalc_face_resign(void *context);
typedef struct {
calc_state_t *cs;
unsigned int mc; // Morse code character
char token[MORSECALC_TOKEN_LEN];
uint8_t idxt;
uint8_t led_is_on;
} morsecalc_state_t;
void morsecalc_reset_token(morsecalc_state_t *mcs);
void morsecalc_input(morsecalc_state_t *mcs);
#define morsecalc_face ((const watch_face_t){ \
morsecalc_face_setup, \
morsecalc_face_activate, \
morsecalc_face_loop, \
morsecalc_face_resign, \
NULL, \
})
#endif // MORSECALC_FACE_H_

View File

@@ -0,0 +1,220 @@
/*
* 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 <math.h>
#include "orrery_face.h"
#include "watch.h"
#include "watch_utility.h"
#include "vsop87a_micro.h" // smaller size, less accurate
#include "vsop87a_milli.h"
#include "astrolib.h"
#define NUM_AVAILABLE_BODIES 9
static const char orrery_celestial_body_names[NUM_AVAILABLE_BODIES][3] = {
"ME", // Mercury
"VE", // Venus
"EA", // Earth
"LU", // Moon (Luna)
"MA", // Mars
"JU", // Jupiter
"SA", // Saturn
"UR", // Uranus
"NE" // Neptune
};
static void _orrery_face_recalculate(orrery_state_t *state) {
watch_date_time_t date_time = watch_rtc_get_date_time();
uint32_t timestamp = watch_utility_date_time_to_unix_time(date_time, movement_get_current_timezone_offset());
date_time = watch_utility_date_time_from_unix_time(timestamp, 0);
double jd = astro_convert_date_to_julian_date(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR, date_time.unit.month, date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
double et = astro_convert_jd_to_julian_millenia_since_j2000(jd);
double r[3] = {0};
switch(state->active_body_index) {
case 0:
vsop87a_milli_getMercury(et, r);
break;
case 1:
vsop87a_milli_getVenus(et, r);
break;
case 2:
vsop87a_milli_getEarth(et, r);
break;
case 3:
{
double earth[3];
double emb[3];
vsop87a_milli_getEarth(et, earth);
vsop87a_milli_getEmb(et, emb);
vsop87a_milli_getMoon(earth, emb, r);
}
break;
case 4:
vsop87a_milli_getMars(et, r);
break;
case 5:
vsop87a_milli_getJupiter(et, r);
break;
case 6:
vsop87a_milli_getSaturn(et, r);
break;
case 7:
vsop87a_milli_getUranus(et, r);
break;
case 8:
vsop87a_milli_getNeptune(et, r);
break;
}
state->coords[0] = r[0];
state->coords[1] = r[1];
state->coords[2] = r[2];
}
static void _orrery_face_update(movement_event_t event, orrery_state_t *state) {
char buf[11];
switch (state->mode) {
case ORRERY_MODE_SELECTING_BODY:
watch_display_string("Orrery", 4);
if (event.subsecond % 2) {
watch_display_string((char *)orrery_celestial_body_names[state->active_body_index], 0);
} else {
watch_display_string(" ", 0);
}
if (event.subsecond == 0) {
watch_display_string(" ", 2);
switch (state->animation_state) {
case 0:
watch_set_pixel(0, 7);
watch_set_pixel(2, 6);
break;
case 1:
watch_set_pixel(1, 7);
watch_set_pixel(2, 9);
break;
case 2:
watch_set_pixel(2, 7);
watch_set_pixel(0, 9);
break;
}
state->animation_state = (state->animation_state + 1) % 3;
}
break;
case ORRERY_MODE_CALCULATING:
watch_clear_display();
// this takes a moment and locks the UI, flash C for "Calculating"
watch_start_character_blink('C', 100);
_orrery_face_recalculate(state);
watch_stop_blink();
state->mode = ORRERY_MODE_DISPLAYING_X;
// fall through
case ORRERY_MODE_DISPLAYING_X:
sprintf(buf, "%s X%6d", orrery_celestial_body_names[state->active_body_index], (int16_t)round(state->coords[0] * 100));
watch_display_string(buf, 0);
break;
case ORRERY_MODE_DISPLAYING_Y:
sprintf(buf, "%s Y%6d", orrery_celestial_body_names[state->active_body_index], (int16_t)round(state->coords[1] * 100));
watch_display_string(buf, 0);
break;
case ORRERY_MODE_DISPLAYING_Z:
sprintf(buf, "%s Z%6d", orrery_celestial_body_names[state->active_body_index], (int16_t)round(state->coords[2] * 100));
watch_display_string(buf, 0);
break;
case ORRERY_MODE_NUM_MODES:
// this case does not happen, but we need it to silence a warning.
break;
}
}
void orrery_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(orrery_state_t));
memset(*context_ptr, 0, sizeof(orrery_state_t));
}
}
void orrery_face_activate(void *context) {
(void) context;
movement_request_tick_frequency(4);
}
bool orrery_face_loop(movement_event_t event, void *context) {
orrery_state_t *state = (orrery_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
case EVENT_TICK:
_orrery_face_update(event, state);
break;
case EVENT_ALARM_BUTTON_UP:
switch (state->mode) {
case ORRERY_MODE_SELECTING_BODY:
// advance to next celestial body (move to calculations with a long press)
state->active_body_index = (state->active_body_index + 1) % NUM_AVAILABLE_BODIES;
break;
case ORRERY_MODE_CALCULATING:
// ignore button press during calculations
break;
case ORRERY_MODE_DISPLAYING_Z:
// at last mode, wrap around
state->mode = ORRERY_MODE_DISPLAYING_X;
break;
default:
// otherwise, advance to next mode
state->mode++;
break;
}
_orrery_face_update(event, state);
break;
case EVENT_ALARM_LONG_PRESS:
if (state->mode == ORRERY_MODE_SELECTING_BODY) {
// celestial body selected! this triggers a calculation in the update method.
state->mode = ORRERY_MODE_CALCULATING;
movement_request_tick_frequency(1);
_orrery_face_update(event, state);
} else if (state->mode != ORRERY_MODE_CALCULATING) {
// in all modes except "doing a calculation", return to the selection screen.
state->mode = ORRERY_MODE_SELECTING_BODY;
movement_request_tick_frequency(4);
_orrery_face_update(event, state);
}
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void orrery_face_resign(void *context) {
orrery_state_t *state = (orrery_state_t *)context;
state->mode = ORRERY_MODE_SELECTING_BODY;
}

View File

@@ -0,0 +1,101 @@
/*
* 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 ORRERY_FACE_H_
#define ORRERY_FACE_H_
/*
* ORRERY face
*
* The Orrery watch face is similar to the Astronomy watch face in that it
* calculates properties of the planets, but instead of calculating their
* positions in the sky, this watch face calculates their absolute locations
* in the solar system. This is only useful if you want to plot the planets
* on graph paper, but hey, you never know!
*
* The controls are identical to the Astronomy watch face: while the title
* screen (“Orrery”) is displayed, you can advance through the available
* planets with repeated short presses on the Alarm button. The available
* planets:
*
* ME - Mercury
* VE - Venus
* EA - Earth
* LU - Luna, the Earths moon
* MA - Mars
* JU - Jupiter
* SA - Saturn
* UR - Uranus
* NE - Neptune
*
* Note that the sun is not available in this menu, as the sun is always at
* (0,0,0) in this calculation.
*
* Long press on the Alarm button to calculate the planets location, and
* after a flashing “C” (for Calculating), you will be presented with the
* planets X coordinate in astronomical units. Short press Alarm to cycle
* through the X, Y and Z coordinates, and then long press Alarm to return
* to planet selection.
*
* The large numbers represent the whole number part, and the two smaller
* numbers (in the seconds place) represent the decimal portion. So if you
* see “SA X 736” and “SA Y -662”, you can read that as an X coordinate of
* 7.36 AU and a Y coordinate of -6.62 AU. You can literally draw a dot at
* (0, 0) to represent the sun, and a dot at (7.36, -6.62) to represent
* Saturn. (The Z coordinates tend to be pretty close to zero, as the
* planets largely orbit on a single plane, the ecliptic.)
*/
#include "movement.h"
typedef enum {
ORRERY_MODE_SELECTING_BODY = 0,
ORRERY_MODE_CALCULATING,
ORRERY_MODE_DISPLAYING_X,
ORRERY_MODE_DISPLAYING_Y,
ORRERY_MODE_DISPLAYING_Z,
ORRERY_MODE_NUM_MODES
} orrery_mode_t;
typedef struct {
orrery_mode_t mode;
uint8_t active_body_index;
double coords[3];
uint8_t animation_state;
} orrery_state_t;
void orrery_face_setup(uint8_t watch_face_index, void ** context_ptr);
void orrery_face_activate(void *context);
bool orrery_face_loop(movement_event_t event, void *context);
void orrery_face_resign(void *context);
#define orrery_face ((const watch_face_t){ \
orrery_face_setup, \
orrery_face_activate, \
orrery_face_loop, \
orrery_face_resign, \
NULL, \
})
#endif // ORRERY_FACE_H_

View File

@@ -0,0 +1,500 @@
/*
* MIT License
*
* Copyright (c) 2023 PrimmR
* Copyright (c) 2024 David Volovskiy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include <string.h>
#include "periodic_face.h"
#define FREQ_FAST 8
#define FREQ 2
static bool _quick_ticks_running;
static uint8_t _ts_ticks = 0;
static int16_t _text_pos;
static const char* _text_looping;
static const char title_text[] = "Periodic Table";
void periodic_face_setup(uint8_t watch_face_index, void **context_ptr)
{
(void)watch_face_index;
if (*context_ptr == NULL)
{
*context_ptr = malloc(sizeof(periodic_state_t));
memset(*context_ptr, 0, sizeof(periodic_state_t));
}
}
void periodic_face_activate(void *context)
{
periodic_state_t *state = (periodic_state_t *)context;
state->atomic_num = 0;
state->mode = 0;
state->selection_index = 0;
_quick_ticks_running = false;
movement_request_tick_frequency(FREQ);
}
typedef struct
{
char symbol[3];
char name[14]; // Longest is Rutherfordium
int16_t year_discovered; // Negative is BC
uint16_t atomic_mass; // In units of 0.01 AMU
uint16_t electronegativity; // In units of 0.01
char group[3];
} element;
typedef enum {
SCREEN_TITLE = 0,
SCREEN_ELEMENT,
SCREEN_ATOMIC_MASS,
SCREEN_DISCOVER_YEAR,
SCREEN_ELECTRONEGATIVITY,
SCREEN_FULL_NAME,
SCREENS_COUNT
} PeriodicScreens;
const char screen_name[SCREENS_COUNT][3] = {
[SCREEN_ATOMIC_MASS] = "am",
[SCREEN_DISCOVER_YEAR] = " y",
[SCREEN_ELECTRONEGATIVITY] = "EL",
[SCREEN_FULL_NAME] = " n",
};
// Comments on the table denote symbols that cannot be displayed
#define MAX_ELEMENT 118
const element table[MAX_ELEMENT] = {
{ .symbol = "H", .name = "Hydrogen", .year_discovered = 1671, .atomic_mass = 101, .electronegativity = 220, .group = " " },
{ .symbol = "HE", .name = "Helium", .year_discovered = 1868, .atomic_mass = 400, .electronegativity = 0, .group = "0" },
{ .symbol = "LI", .name = "Lithium", .year_discovered = 1817, .atomic_mass = 694, .electronegativity = 98, .group = "1" },
{ .symbol = "BE", .name = "Beryllium", .year_discovered = 1798, .atomic_mass = 901, .electronegativity = 157, .group = "2" },
{ .symbol = "B", .name = "Boron", .year_discovered = 1787, .atomic_mass = 1081, .electronegativity = 204, .group = "3" },
{ .symbol = "C", .name = "Carbon", .year_discovered = -26000, .atomic_mass = 1201, .electronegativity = 255, .group = "4" },
{ .symbol = "N", .name = "Nitrogen", .year_discovered = 1772, .atomic_mass = 1401, .electronegativity = 304, .group = "5" },
{ .symbol = "O", .name = "Oxygen", .year_discovered = 1771, .atomic_mass = 1600, .electronegativity = 344, .group = "6" },
{ .symbol = "F", .name = "Fluorine", .year_discovered = 1771, .atomic_mass = 1900, .electronegativity = 398, .group = "7" },
{ .symbol = "NE", .name = "Neon", .year_discovered = 1898, .atomic_mass = 2018, .electronegativity = 0, .group = "0" },
{ .symbol = "NA", .name = "Sodium", .year_discovered = 1702, .atomic_mass = 2299, .electronegativity = 93, .group = "1" },
{ .symbol = "MG", .name = "Magnesium", .year_discovered = 1755, .atomic_mass = 2431, .electronegativity = 131, .group = "2" },
{ .symbol = "AL", .name = "Aluminium", .year_discovered = 1746, .atomic_mass = 2698, .electronegativity = 161, .group = "3" },
{ .symbol = "SI", .name = "Silicon", .year_discovered = 1739, .atomic_mass = 2809, .electronegativity = 190, .group = "4" },
{ .symbol = "P", .name = "Phosphorus", .year_discovered = 1669, .atomic_mass = 3097, .electronegativity = 219, .group = "5" },
{ .symbol = "S", .name = "Sulfur", .year_discovered = -2000, .atomic_mass = 3206, .electronegativity = 258, .group = "6" },
{ .symbol = "CL", .name = "Chlorine", .year_discovered = 1774, .atomic_mass = 3545., .electronegativity = 316, .group = "7" },
{ .symbol = "AR", .name = "Argon", .year_discovered = 1894, .atomic_mass = 3995., .electronegativity = 0, .group = "0" },
{ .symbol = "K", .name = "Potassium", .year_discovered = 1702, .atomic_mass = 3910, .electronegativity = 82, .group = "1" },
{ .symbol = "CA", .name = "Calcium", .year_discovered = 1739, .atomic_mass = 4008, .electronegativity = 100, .group = "2" },
{ .symbol = "SC", .name = "Scandium", .year_discovered = 1879, .atomic_mass = 4496, .electronegativity = 136, .group = " T" },
{ .symbol = "TI", .name = "Titanium", .year_discovered = 1791, .atomic_mass = 4787, .electronegativity = 154, .group = " T" },
{ .symbol = "W", .name = "Vanadium", .year_discovered = 1801, .atomic_mass = 5094, .electronegativity = 163, .group = " T" },
{ .symbol = "CR", .name = "Chromium", .year_discovered = 1797, .atomic_mass = 5200, .electronegativity = 166, .group = " T" },
{ .symbol = "MN", .name = "Manganese", .year_discovered = 1774, .atomic_mass = 5494, .electronegativity = 155, .group = " T" },
{ .symbol = "FE", .name = "Iron", .year_discovered = -5000, .atomic_mass = 5585, .electronegativity = 183, .group = " T" },
{ .symbol = "CO", .name = "Cobalt", .year_discovered = 1735, .atomic_mass = 5893, .electronegativity = 188, .group = " T" },
{ .symbol = "NI", .name = "Nickel", .year_discovered = 1751, .atomic_mass = 5869, .electronegativity = 191, .group = " T" },
{ .symbol = "CU", .name = "Copper", .year_discovered = -9000, .atomic_mass = 6355, .electronegativity = 190, .group = " T" },
{ .symbol = "ZN", .name = "Zinc", .year_discovered = -1000, .atomic_mass = 6538, .electronegativity = 165, .group = " T" },
{ .symbol = "GA", .name = "Gallium", .year_discovered = 1875, .atomic_mass = 6972, .electronegativity = 181, .group = "3" },
{ .symbol = "GE", .name = "Germanium", .year_discovered = 1886, .atomic_mass = 7263, .electronegativity = 201, .group = "4" },
{ .symbol = "AS", .name = "Arsenic", .year_discovered = 300, .atomic_mass = 7492, .electronegativity = 218, .group = "5" },
{ .symbol = "SE", .name = "Selenium", .year_discovered = 1817, .atomic_mass = 7897, .electronegativity = 255, .group = "6" },
{ .symbol = "BR", .name = "Bromine", .year_discovered = 1825, .atomic_mass = 7990., .electronegativity = 296, .group = "7" },
{ .symbol = "KR", .name = "Krypton", .year_discovered = 1898, .atomic_mass = 8380, .electronegativity = 300, .group = "0" },
{ .symbol = "RB", .name = "Rubidium", .year_discovered = 1861, .atomic_mass = 8547, .electronegativity = 82, .group = "1" },
{ .symbol = "SR", .name = "Strontium", .year_discovered = 1787, .atomic_mass = 8762, .electronegativity = 95, .group = "2" },
{ .symbol = "Y", .name = "Yttrium", .year_discovered = 1794, .atomic_mass = 8891, .electronegativity = 122, .group = " T" },
{ .symbol = "ZR", .name = "Zirconium", .year_discovered = 1789, .atomic_mass = 9122, .electronegativity = 133, .group = " T" },
{ .symbol = "NB", .name = "Niobium", .year_discovered = 1801, .atomic_mass = 9291, .electronegativity = 160, .group = " T" },
{ .symbol = "MO", .name = "Molybdenum", .year_discovered = 1778, .atomic_mass = 9595, .electronegativity = 216, .group = " T" },
{ .symbol = "TC", .name = "Technetium", .year_discovered = 1937, .atomic_mass = 9700, .electronegativity = 190, .group = " T" },
{ .symbol = "RU", .name = "Ruthenium", .year_discovered = 1844, .atomic_mass = 10107, .electronegativity = 220, .group = " T" },
{ .symbol = "RH", .name = "Rhodium", .year_discovered = 1804, .atomic_mass = 10291, .electronegativity = 228, .group = " T" },
{ .symbol = "PD", .name = "Palladium", .year_discovered = 1802, .atomic_mass = 10642, .electronegativity = 220, .group = " T" },
{ .symbol = "AG", .name = "Silver", .year_discovered = -5000, .atomic_mass = 10787, .electronegativity = 193, .group = " T" },
{ .symbol = "CD", .name = "Cadmium", .year_discovered = 1817, .atomic_mass = 11241, .electronegativity = 169, .group = " T" },
{ .symbol = "IN", .name = "Indium", .year_discovered = 1863, .atomic_mass = 11482, .electronegativity = 178, .group = "3" },
{ .symbol = "SN", .name = "Tin", .year_discovered = -3500, .atomic_mass = 11871, .electronegativity = 196, .group = "4" },
{ .symbol = "SB", .name = "Antimony", .year_discovered = -3000, .atomic_mass = 12176, .electronegativity = 205, .group = "5" },
{ .symbol = "TE", .name = "Tellurium", .year_discovered = 1782, .atomic_mass = 12760, .electronegativity = 210, .group = "6" },
{ .symbol = "I", .name = "Iodine", .year_discovered = 1811, .atomic_mass = 12690, .electronegativity = 266, .group = "7" },
{ .symbol = "XE", .name = "Xenon", .year_discovered = 1898, .atomic_mass = 13129, .electronegativity = 260, .group = "0" },
{ .symbol = "CS", .name = "Caesium", .year_discovered = 1860, .atomic_mass = 13291, .electronegativity = 79, .group = "1" },
{ .symbol = "BA", .name = "Barium", .year_discovered = 1772, .atomic_mass = 13733., .electronegativity = 89, .group = "2" },
{ .symbol = "LA", .name = "Lanthanum", .year_discovered = 1838, .atomic_mass = 13891, .electronegativity = 110, .group = "1a" },
{ .symbol = "CE", .name = "Cerium", .year_discovered = 1803, .atomic_mass = 14012, .electronegativity = 112, .group = "1a" },
{ .symbol = "PR", .name = "Praseodymium", .year_discovered = 1885, .atomic_mass = 14091, .electronegativity = 113, .group = "1a" },
{ .symbol = "ND", .name = "Neodymium", .year_discovered = 1841, .atomic_mass = 14424, .electronegativity = 114, .group = "1a" },
{ .symbol = "PM", .name = "Promethium", .year_discovered = 1945, .atomic_mass = 14500, .electronegativity = 113, .group = "1a" },
{ .symbol = "SM", .name = "Samarium", .year_discovered = 1879, .atomic_mass = 15036., .electronegativity = 117, .group = "1a" },
{ .symbol = "EU", .name = "Europium", .year_discovered = 1896, .atomic_mass = 15196, .electronegativity = 120, .group = "1a" },
{ .symbol = "GD", .name = "Gadolinium", .year_discovered = 1880, .atomic_mass = 15725, .electronegativity = 120, .group = "1a" },
{ .symbol = "TB", .name = "Terbium", .year_discovered = 1843, .atomic_mass = 15893, .electronegativity = 120, .group = "1a" },
{ .symbol = "DY", .name = "Dysprosium", .year_discovered = 1886, .atomic_mass = 16250, .electronegativity = 122, .group = "1a" },
{ .symbol = "HO", .name = "Holmium", .year_discovered = 1878, .atomic_mass = 16493, .electronegativity = 123, .group = "1a" },
{ .symbol = "ER", .name = "Erbium", .year_discovered = 1843, .atomic_mass = 16726, .electronegativity = 124, .group = "1a" },
{ .symbol = "TM", .name = "Thulium", .year_discovered = 1879, .atomic_mass = 16893, .electronegativity = 125, .group = "1a" },
{ .symbol = "YB", .name = "Ytterbium", .year_discovered = 1878, .atomic_mass = 17305, .electronegativity = 110, .group = "1a" },
{ .symbol = "LU", .name = "Lutetium", .year_discovered = 1906, .atomic_mass = 17497, .electronegativity = 127, .group = "1a" },
{ .symbol = "HF", .name = "Hafnium", .year_discovered = 1922, .atomic_mass = 17849, .electronegativity = 130, .group = " T" },
{ .symbol = "TA", .name = "Tantalum", .year_discovered = 1802, .atomic_mass = 18095, .electronegativity = 150, .group = " T" },
{ .symbol = "W", .name = "Tungsten", .year_discovered = 1781, .atomic_mass = 18384, .electronegativity = 236, .group = " T" },
{ .symbol = "RE", .name = "Rhenium", .year_discovered = 1908, .atomic_mass = 18621, .electronegativity = 190, .group = " T" },
{ .symbol = "OS", .name = "Osmium", .year_discovered = 1803, .atomic_mass = 19023, .electronegativity = 220, .group = " T" },
{ .symbol = "IR", .name = "Iridium", .year_discovered = 1803, .atomic_mass = 19222, .electronegativity = 220, .group = " T" },
{ .symbol = "PT", .name = "Platinum", .year_discovered = -600, .atomic_mass = 19508, .electronegativity = 228, .group = " T" },
{ .symbol = "AU", .name = "Gold", .year_discovered = -6000, .atomic_mass = 19697, .electronegativity = 254, .group = " T" },
{ .symbol = "HG", .name = "Mercury", .year_discovered = -1500, .atomic_mass = 20059, .electronegativity = 200, .group = " T" },
{ .symbol = "TL", .name = "Thallium", .year_discovered = 1861, .atomic_mass = 20438, .electronegativity = 162, .group = "3" },
{ .symbol = "PB", .name = "Lead", .year_discovered = -7000, .atomic_mass = 20720, .electronegativity = 187, .group = "4" },
{ .symbol = "BI", .name = "Bismuth", .year_discovered = 1500, .atomic_mass = 20898, .electronegativity = 202, .group = "5" },
{ .symbol = "PO", .name = "Polonium", .year_discovered = 1898, .atomic_mass = 20900, .electronegativity = 200, .group = "6" },
{ .symbol = "AT", .name = "Astatine", .year_discovered = 1940, .atomic_mass = 21000, .electronegativity = 220, .group = "7" },
{ .symbol = "RN", .name = "Radon", .year_discovered = 1899, .atomic_mass = 22200, .electronegativity = 220, .group = "0" },
{ .symbol = "FR", .name = "Francium", .year_discovered = 1939, .atomic_mass = 22300, .electronegativity = 79, .group = "1" },
{ .symbol = "RA", .name = "Radium", .year_discovered = 1898, .atomic_mass = 22600, .electronegativity = 90, .group = "2" },
{ .symbol = "AC", .name = "Actinium", .year_discovered = 1902, .atomic_mass = 22700, .electronegativity = 110, .group = "Ac" },
{ .symbol = "TH", .name = "Thorium", .year_discovered = 1829, .atomic_mass = 23204, .electronegativity = 130, .group = "Ac" },
{ .symbol = "PA", .name = "Protactinium", .year_discovered = 1913, .atomic_mass = 23104, .electronegativity = 150, .group = "Ac" },
{ .symbol = "U", .name = "Uranium", .year_discovered = 1789, .atomic_mass = 23803, .electronegativity = 138, .group = "Ac" },
{ .symbol = "NP", .name = "Neptunium", .year_discovered = 1940, .atomic_mass = 23700, .electronegativity = 136, .group = "Ac" },
{ .symbol = "PU", .name = "Plutonium", .year_discovered = 1941, .atomic_mass = 24400, .electronegativity = 128, .group = "Ac" },
{ .symbol = "AM", .name = "Americium", .year_discovered = 1944, .atomic_mass = 24300, .electronegativity = 113, .group = "Ac" },
{ .symbol = "CM", .name = "Curium", .year_discovered = 1944, .atomic_mass = 24700, .electronegativity = 128, .group = "Ac" },
{ .symbol = "BK", .name = "Berkelium", .year_discovered = 1949, .atomic_mass = 24700, .electronegativity = 130, .group = "Ac" },
{ .symbol = "CF", .name = "Californium", .year_discovered = 1950, .atomic_mass = 25100, .electronegativity = 130, .group = "Ac" },
{ .symbol = "ES", .name = "Einsteinium", .year_discovered = 1952, .atomic_mass = 25200, .electronegativity = 130, .group = "Ac" },
{ .symbol = "FM", .name = "Fermium", .year_discovered = 1953, .atomic_mass = 25700, .electronegativity = 130, .group = "Ac" },
{ .symbol = "MD", .name = "Mendelevium", .year_discovered = 1955, .atomic_mass = 25800, .electronegativity = 130, .group = "Ac" },
{ .symbol = "NO", .name = "Nobelium", .year_discovered = 1965, .atomic_mass = 25900, .electronegativity = 130, .group = "Ac" },
{ .symbol = "LR", .name = "Lawrencium", .year_discovered = 1961, .atomic_mass = 26600, .electronegativity = 130, .group = "Ac" },
{ .symbol = "RF", .name = "Rutherfordium", .year_discovered = 1969, .atomic_mass = 26700, .electronegativity = 0, .group = " T" },
{ .symbol = "DB", .name = "Dubnium", .year_discovered = 1970, .atomic_mass = 26800, .electronegativity = 0, .group = " T" },
{ .symbol = "SG", .name = "Seaborgium", .year_discovered = 1974, .atomic_mass = 26700, .electronegativity = 0, .group = " T" },
{ .symbol = "BH", .name = "Bohrium", .year_discovered = 1981, .atomic_mass = 27000, .electronegativity = 0, .group = " T" },
{ .symbol = "HS", .name = "Hassium", .year_discovered = 1984, .atomic_mass = 27100, .electronegativity = 0, .group = " T" },
{ .symbol = "MT", .name = "Meitnerium", .year_discovered = 1982, .atomic_mass = 27800, .electronegativity = 0, .group = " T" },
{ .symbol = "DS", .name = "Darmstadtium", .year_discovered = 1994, .atomic_mass = 28100, .electronegativity = 0, .group = " T" },
{ .symbol = "RG", .name = "Roentgenium", .year_discovered = 1994, .atomic_mass = 28200, .electronegativity = 0, .group = " T" },
{ .symbol = "CN", .name = "Copernicium", .year_discovered = 1996, .atomic_mass = 28500, .electronegativity = 0, .group = " T" },
{ .symbol = "NH", .name = "Nihonium", .year_discovered = 2004, .atomic_mass = 28600, .electronegativity = 0, .group = "3" },
{ .symbol = "FL", .name = "Flerovium", .year_discovered = 1999, .atomic_mass = 28900, .electronegativity = 0, .group = "4" },
{ .symbol = "MC", .name = "Moscovium", .year_discovered = 2003, .atomic_mass = 29000, .electronegativity = 0, .group = "5" },
{ .symbol = "LW", .name = "Livermorium", .year_discovered = 2000, .atomic_mass = 29300, .electronegativity = 0, .group = "6" },
{ .symbol = "TS", .name = "Tennessine", .year_discovered = 2009, .atomic_mass = 29400, .electronegativity = 0, .group = "7" },
{ .symbol = "OG", .name = "Oganesson", .year_discovered = 2002, .atomic_mass = 29400, .electronegativity = 0, .group = "0" },
};
static void _make_upper(char *string) {
size_t i = 0;
while(string[i] != 0) {
if (string[i] >= 'a' && string[i] <= 'z')
string[i]-=32; // 32 = 'a'-'A'
i++;
}
}
static void _display_element(periodic_state_t *state)
{
char buf[9];
char ele[3];
uint8_t atomic_num = state->atomic_num;
strcpy(ele, table[atomic_num - 1].symbol);
_make_upper(ele);
sprintf(buf, "%2s%3d %-2s", table[atomic_num - 1].group, atomic_num, ele);
watch_display_string(buf, 2);
}
static void _display_atomic_mass(periodic_state_t *state)
{
char buf[11];
uint16_t mass = table[state->atomic_num - 1].atomic_mass;
uint16_t integer = mass / 100;
uint16_t decimal = mass % 100;
if (decimal == 0)
sprintf(buf, "%-2s%2s%4d", table[state->atomic_num - 1].symbol, screen_name[state->mode], integer);
else
sprintf(buf, "%-2s%2s%3d_%.2d", table[state->atomic_num - 1].symbol, screen_name[state->mode], integer, decimal);
watch_display_string(buf, 0);
}
static void _display_year_discovered(periodic_state_t *state)
{
char buf[11];
char year_buf[7];
int16_t year = table[state->atomic_num - 1].year_discovered;
if (abs(year) > 9999)
sprintf(year_buf, "---- ");
else
sprintf(year_buf, "%4d ", abs(year));
if (year < 0) {
year_buf[4] = 'b';
year_buf[5] = 'c';
}
sprintf(buf, "%-2s%-2s%s", table[state->atomic_num - 1].symbol, screen_name[state->mode], year_buf);
watch_display_string(buf, 0);
}
static void _display_name(periodic_state_t *state)
{
char buf[11];
_text_looping = table[state->atomic_num - 1].name;
_text_pos = 0;
sprintf(buf, "%-2s%-2s%s", table[state->atomic_num - 1].symbol, screen_name[state->mode], table[state->atomic_num - 1].name);
watch_display_string(buf, 0);
}
static void _display_electronegativity(periodic_state_t *state)
{
char buf[11];
uint16_t electronegativity = table[state->atomic_num - 1].electronegativity;
uint16_t integer = electronegativity / 100;
uint16_t decimal = electronegativity % 100;
if (decimal == 0)
sprintf(buf, "%-2s%2s%4d", table[state->atomic_num - 1].symbol, screen_name[state->mode], integer);
else
sprintf(buf, "%-2s%2s%3d_%.2d", table[state->atomic_num - 1].symbol, screen_name[state->mode], integer, decimal);
watch_display_string(buf, 0);
}
static void start_quick_cyc(void){
_quick_ticks_running = true;
movement_request_tick_frequency(FREQ_FAST);
}
static void stop_quick_cyc(void){
_quick_ticks_running = false;
movement_request_tick_frequency(FREQ);
}
static int16_t _loop_text(const char* text, int8_t curr_loc, uint8_t char_len){
// if curr_loc, then use that many ticks as a delay before looping
char buf[15];
uint8_t next_pos;
uint8_t text_len = strlen(text);
uint8_t pos = 10 - char_len;
if (curr_loc == -1) curr_loc = 0; // To avoid double-showing the 0
if (char_len >= text_len || curr_loc < 0) {
sprintf(buf, "%s", text);
watch_display_string(buf, pos);
if (curr_loc < 0) return ++curr_loc;
return 0;
}
else if (curr_loc == (text_len + 1))
curr_loc = 0;
next_pos = curr_loc + 1;
sprintf(buf, "%.6s %.6s", text + curr_loc, text);
watch_display_string(buf, pos);
return next_pos;
}
static void _display_title(periodic_state_t *state){
state->atomic_num = 0;
watch_clear_colon();
watch_clear_all_indicators();
_text_looping = title_text;
_text_pos = FREQ * -1;
_text_pos = _loop_text(_text_looping, _text_pos, 5);
}
static void _display_screen(periodic_state_t *state, bool should_sound){
watch_clear_display();
watch_clear_all_indicators();
switch (state->mode)
{
case SCREEN_TITLE:
_display_title(state);
break;
case SCREEN_ELEMENT:
case SCREENS_COUNT:
_display_element(state);
break;
case SCREEN_ATOMIC_MASS:
_display_atomic_mass(state);
break;
case SCREEN_DISCOVER_YEAR:
_display_year_discovered(state);
break;
case SCREEN_ELECTRONEGATIVITY:
_display_electronegativity(state);
break;
case SCREEN_FULL_NAME:
_display_name(state);
break;
}
if (should_sound) watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
}
static void _handle_forward(periodic_state_t *state, bool should_sound){
state->atomic_num = (state->atomic_num % MAX_ELEMENT) + 1; // Wraps back to 1
state->mode = SCREEN_ELEMENT;
_display_screen(state, false);
if (should_sound) watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
}
static void _handle_backward(periodic_state_t *state, bool should_sound){
if (state->atomic_num <= 1) state->atomic_num = MAX_ELEMENT;
else state->atomic_num = state->atomic_num - 1;
state->mode = SCREEN_ELEMENT;
_display_screen(state, false);
if (should_sound) watch_buzzer_play_note(BUZZER_NOTE_A6, 50);
}
static void _handle_mode_still_pressed(periodic_state_t *state, bool should_sound) {
if (_ts_ticks != 0){
if (!HAL_GPIO_BTN_MODE_read()) {
_ts_ticks = 0;
return;
}
else if (--_ts_ticks == 0){
switch (state->mode)
{
case SCREEN_TITLE:
movement_move_to_face(0);
return;
case SCREEN_ELEMENT:
state->mode = SCREEN_TITLE;
_display_screen(state, should_sound);
break;
default:
state->mode = SCREEN_ELEMENT;
_display_screen(state, should_sound);
break;
}
_ts_ticks = 2;
}
}
}
bool periodic_face_loop(movement_event_t event, void *context)
{
periodic_state_t *state = (periodic_state_t *)context;
switch (event.event_type)
{
case EVENT_ACTIVATE:
state->mode = SCREEN_TITLE;
_display_screen(state, false);
break;
case EVENT_TICK:
if (state->mode == SCREEN_TITLE) _text_pos = _loop_text(_text_looping, _text_pos, 5);
else if (state->mode == SCREEN_FULL_NAME) _text_pos = _loop_text(_text_looping, _text_pos, 6);
if (_quick_ticks_running) {
if (HAL_GPIO_BTN_LIGHT_read()) _handle_backward(state, false);
else if (HAL_GPIO_BTN_ALARM_read()) _handle_forward(state, false);
else stop_quick_cyc();
}
_handle_mode_still_pressed(state, movement_button_should_sound());
break;
case EVENT_LIGHT_BUTTON_UP:
if (state->mode <= SCREEN_ELEMENT) {
_handle_backward(state, movement_button_should_sound());
}
else {
state->mode = SCREEN_ELEMENT;
_display_screen(state, movement_button_should_sound());
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_ALARM_BUTTON_UP:
if (state->mode <= SCREEN_ELEMENT) {
_handle_forward(state, movement_button_should_sound());
}
else {
state->mode = SCREEN_ELEMENT;
_display_screen(state, movement_button_should_sound());
}
break;
case EVENT_ALARM_LONG_PRESS:
if (state->mode <= SCREEN_ELEMENT) {
start_quick_cyc();
_handle_forward(state, movement_button_should_sound());
}
break;
case EVENT_LIGHT_LONG_PRESS:
if (state->mode <= SCREEN_ELEMENT) {
start_quick_cyc();
_handle_backward(state, movement_button_should_sound());
}
else {
movement_illuminate_led();
}
break;
case EVENT_MODE_BUTTON_UP:
if (state->mode == SCREEN_TITLE) movement_move_to_next_face();
else {
state->mode = (state->mode + 1) % SCREENS_COUNT;
if (state->mode == SCREEN_TITLE)
state->mode = (state->mode + 1) % SCREENS_COUNT;
if (state->mode == SCREEN_ELEMENT){
_display_screen(state, false);
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_A6, 50);
}
else
_display_screen(state, movement_button_should_sound());
}
break;
case EVENT_MODE_LONG_PRESS:
switch (state->mode)
{
case SCREEN_TITLE:
movement_move_to_face(0);
return true;
case SCREEN_ELEMENT:
state->mode = SCREEN_TITLE;
_display_screen(state, movement_button_should_sound());
break;
default:
state->mode = SCREEN_ELEMENT;
_display_screen(state, movement_button_should_sound());
break;
}
_ts_ticks = 2;
break;
case EVENT_TIMEOUT:
// Display title after timeout
if (state->mode == SCREEN_TITLE) break;
state->mode = SCREEN_TITLE;
_display_screen(state, false);
break;
case EVENT_LOW_ENERGY_UPDATE:
// Display static title and tick animation during LE
watch_display_string("Pd Table", 0);
watch_start_sleep_animation(500);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void periodic_face_resign(void *context)
{
(void)context;
// handle any cleanup before your watch face goes off-screen.
}

View File

@@ -0,0 +1,89 @@
/*
* MIT License
*
* Copyright (c) 2023 PrimmR
* Copyright (c) 2024 David Volovskiy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef PERIODIC_FACE_H_
#define PERIODIC_FACE_H_
#include "movement.h"
/*
* Periodic Table Face
* Allows for viewing data of the Periodic Table on your wrist.
* When looking at an element, it'll show you the atomic number on the center of the screen,
* symbol on the right, and it's group on the top-right.
* Pressing the mode button will cycle through the pages.
* Page 1: Atomic Mass
* Page 2: Year Discovered
* Page 3: Electronegativity
* Page 4: Full Name of the Element
*
* Controls:
* Mode Press
* On Title: Next Screen
* Else: Cycle through info of an element
* Mode Hold
* On Title: First Screen
* On Element Symbol Screen: Go to Title Screen
* Else: Go to Symbol Screen of current element
* If you are in a subscreen and just keep holding MODE, you will go through all of these menus without needing to depress.
*
* Light Press
* On Title or Element Symbol Screen: Previous Element
* Else: Display currenlt-selected element symbol page
* Light Hold
* On Title Screen or Element Symbol: Fast Cycle through Previous Elements
* Else: Activate LED backlight
*
* Alarm Press
* On Title or Element Symbol Screen: Next Element
* Else: Display currenlt-selected element symbol page
* Alarm Hold
* On Title Screen or Element Symbol: Fast Cycle through Next Elements
*/
#define MODE_VIEW 0
#define MODE_SELECT 1
typedef struct {
uint8_t atomic_num;
uint8_t mode;
uint8_t selection_index;
} periodic_state_t;
void periodic_face_setup(uint8_t watch_face_index, void ** context_ptr);
void periodic_face_activate(void *context);
bool periodic_face_loop(movement_event_t event, void *context);
void periodic_face_resign(void *context);
#define periodic_face ((const watch_face_t){ \
periodic_face_setup, \
periodic_face_activate, \
periodic_face_loop, \
periodic_face_resign, \
NULL, \
})
#endif // PERIODIC_FACE_H_

View File

@@ -0,0 +1,399 @@
/*
* MIT License
*
* Copyright (c) 2023 Tobias Raayoni Last / @randogoth
*
* 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 <math.h>
#include "sunriset.h"
#include "watch.h"
#include "watch_utility.h"
#include "planetary_hours_face.h"
#if __EMSCRIPTEN__
#include <emscripten.h>
#endif
// STATIC FUNCTIONS AND CONSTANTS /////////////////////////////////////////////
/** @brief Planetary rulers in the Chaldean order from slowest to fastest
* @details Planetary rulers in the Chaldean order from slowest to fastest:
* Jupiter, Mars, Sun, Venus, Mercury, Moon
*/
static const char planets[7][3] = {"Sa", "Ju", "Ma", "So", "Ve", "Me", "Lu"}; // Latin
static const char planetes[7][3] = {"Ch", "Ze", "Ar", "He", "Af", "Hr", "Se"}; // Greek
/** @brief Ruler of each weekday for easy lookup
*/
static const uint8_t plindex[7] = {3, 6, 2, 5, 1, 4, 0}; // day ruler index
/** @brief Astrological symbol for each planet
*/
static void _planetary_icon(uint8_t planet) {
watch_clear_pixel(0, 13);
watch_clear_pixel(0, 14);
watch_clear_pixel(1, 13);
watch_clear_pixel(1, 14);
watch_clear_pixel(1, 15);
watch_clear_pixel(2, 13);
watch_clear_pixel(2, 14);
watch_clear_pixel(2, 15);
switch (planet) {
case 0: // Saturn
watch_set_pixel(0, 14);
watch_set_pixel(2, 14);
watch_set_pixel(1, 15);
watch_set_pixel(2, 13);
break;
case 1: // Jupiter
watch_set_pixel(0, 14);
watch_set_pixel(1, 15);
watch_set_pixel(1, 14);
break;
case 2: // Mars
watch_set_pixel(2, 14);
watch_set_pixel(2, 15);
watch_set_pixel(1, 15);
watch_set_pixel(2, 13);
watch_set_pixel(1, 13);\
break;
case 3: // Sol
watch_set_pixel(0, 14);
watch_set_pixel(2, 14);
watch_set_pixel(1, 13);
watch_set_pixel(2, 13);
watch_set_pixel(0, 13);
watch_set_pixel(2, 15);
break;
case 4: // Venus
watch_set_pixel(0, 14);
watch_set_pixel(0, 13);
watch_set_pixel(1, 13);
watch_set_pixel(1, 15);
watch_set_pixel(1, 14);
break;
case 5: // Mercury
watch_set_pixel(0, 14);
watch_set_pixel(1, 13);
watch_set_pixel(1, 14);
watch_set_pixel(1, 15);
watch_set_pixel(2, 15);
break;
case 6: // Luna
watch_set_pixel(2, 14);
watch_set_pixel(2, 15);
watch_set_pixel(2, 13);
break;
}
}
/** @details A solar phase can be a day phase between sunrise and sunset or an alternating night phase.
* This function calculates the start and end of the current phase based on a given geographic location.
* It also calculates the start of the next following phase.
*/
static void _planetary_solar_phases(planetary_hours_state_t *state) {
uint8_t phase, h;
double sunrise, sunset;
double hour_duration, next_hour_duration;
uint32_t now_epoch;
uint32_t sunrise_epoch_today, sunset_epoch_today, midnight_epoch_today;
uint32_t sunset_epoch_yesterday, midnight_epoch_yesterday;
uint32_t sunrise_epoch_tomorrow, sunset_epoch_tomorrow, midnight_epoch_tomorrow;
movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1);
// check if we have a location. If not, display error
if (movement_location.reg == 0) {
watch_display_string(" no Loc", 0);
state->no_location = true;
return;
}
// location detected
state->no_location = false;
watch_date_time_t date_time = watch_rtc_get_date_time(); // the current local date / time
watch_date_time_t utc_now = watch_utility_date_time_convert_zone(date_time, movement_get_current_timezone_offset(), 0); // the current date / time in UTC
watch_date_time_t scratch_time; // scratchpad, contains different values at different times
watch_date_time_t midnight;
scratch_time.reg = midnight.reg = utc_now.reg;
midnight.unit.hour = midnight.unit.minute = midnight.unit.second = 0; // start of the day at midnight
// get location coordinate
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;
// save UTC offset
state->utc_offset = ((double)movement_get_current_timezone_offset()) / 3600.0;
// calculate sunrise and sunset of current day in decimal hours after midnight
sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &sunrise, &sunset);
// calculate sunrise and sunset UNIX timestamps
midnight_epoch_today = watch_utility_date_time_to_unix_time(midnight, 0);
sunrise_epoch_today = midnight_epoch_today + sunrise * 3600;
sunset_epoch_today = midnight_epoch_today + sunset * 3600;
// go back to yesterday and calculate sunset
midnight_epoch_yesterday = midnight_epoch_today - 86400;
scratch_time = watch_utility_date_time_from_unix_time(midnight_epoch_yesterday, 0);
sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &sunrise, &sunset);
sunset_epoch_yesterday = midnight_epoch_yesterday + sunset * 3600;
// go to tomorrow and calculate sunrise and sunset
midnight_epoch_tomorrow = midnight_epoch_today + 86400;
scratch_time = watch_utility_date_time_from_unix_time(midnight_epoch_tomorrow, 0);
sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &sunrise, &sunset);
sunrise_epoch_tomorrow = midnight_epoch_tomorrow + sunrise * 3600;
sunset_epoch_tomorrow = midnight_epoch_tomorrow + sunset * 3600;
// get UNIX epoch time
now_epoch = watch_utility_date_time_to_unix_time(utc_now, 0);
// by default we assume it is daytime (phase 1) between sunrise and sunset
phase = 1;
state->phase_start = sunrise_epoch_today;
state->phase_end = sunset_epoch_today;
state->phase_next = sunrise_epoch_tomorrow;
state->start_at_night = false;
// night time calculations
if ( now_epoch < sunrise_epoch_today && now_epoch < sunset_epoch_today ) phase = 0; // morning before dawn
if ( now_epoch > sunrise_epoch_today && now_epoch >= sunset_epoch_today ) phase = 2; // evening after dusk
// phase 0: we are before sunrise
if ( phase == 0) {
state->phase_start = sunset_epoch_yesterday;
state->phase_end = sunrise_epoch_today;
state->phase_next = sunset_epoch_today;
state->start_at_night = true;
}
// phase 2: we are after sunset
if ( phase == 2) {
state->phase_start = sunset_epoch_today;
state->phase_end = sunrise_epoch_tomorrow;
state->phase_next = sunset_epoch_tomorrow;
state->start_at_night = true;
}
// calculate the duration of a planetary hour during this and the next solar phase
hour_duration = ( state->phase_end - state->phase_start ) / 12.0;
next_hour_duration = ( state->phase_next - state->phase_end ) / 12.0;
// populate list of 24 planetary hour start points in UNIX timestamp format
// starting from the beginning of the current phase
for ( h = 0; h < 24; h++ ) {
if ( h < 12 ) state->planetary_hours[h] = state->phase_start + h * hour_duration; // current phase
else state->planetary_hours[h] = state->phase_end + ( h - 12 ) * next_hour_duration; // next phase
}
// initialize
state->hour = 0;
state->ruler = 0;
state->skip_to_current = true;
}
/** @details A planetary hour is one of exactly twelve hours of a solar phase. Its length varies.
* This function calculates the current planetary hour and divides it up into relative minutes and seconds.
* It also calculates the current planetary ruler of the hour and of the day.
*/
static void _planetary_hours(planetary_hours_state_t *state) {
char buf[14];
char ruler[3];
uint8_t weekday, planet, planetary_hour;
uint32_t current_hour_epoch;
watch_date_time_t scratch_time;
// check if we have a location. If not, display error
if ( state->no_location ) {
watch_display_string(" no Loc", 0);
return;
}
// get current time
watch_date_time_t date_time = watch_rtc_get_date_time(); // the current local date / time
watch_date_time_t utc_now = watch_utility_date_time_convert_zone(date_time, movement_get_current_timezone_offset(), 0); // the current date / time in UTC
current_hour_epoch = watch_utility_date_time_to_unix_time(utc_now, 0);
// set the current planetary hour as default screen
if ( state->skip_to_current ) {
state->hour = ( current_hour_epoch - state->phase_start ) / (( state->phase_end - state->phase_start ) / 12.0);
state->skip_to_current = false;
}
// when current phase ends calculate the next phase
if ( watch_utility_date_time_to_unix_time(utc_now, 0) >= state->phase_end ) {
_planetary_solar_phases(state);
return;
}
if (movement_clock_mode_24h()) watch_set_indicator(WATCH_INDICATOR_24H);
// roll over hour iterator
if ( state->hour < 0 ) state->hour = 23;
if ( state->hour > 23 ) state->hour = 0;
if ( state->ruler < 0 ) state->hour = 2;
if ( state->ruler > 2 ) state->hour = 0;
// clear indicators
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_clear_indicator(WATCH_INDICATOR_LAP);
// display bell indicator when displaying the current planetary hour
if ( state->hour < 24 )
if ( current_hour_epoch >= state->planetary_hours[state->hour] && current_hour_epoch < state->planetary_hours[state->hour + 1]) {
watch_set_indicator(WATCH_INDICATOR_BELL);
}
// display LAP indicator when the hours of the next phase belong to the next day
if ( state->start_at_night == true && state->hour > 11 )
watch_set_indicator(WATCH_INDICATOR_LAP);
// determine weekday from start of current phase
scratch_time = watch_utility_date_time_from_unix_time(state->phase_start, 0);
scratch_time = watch_utility_date_time_convert_zone(scratch_time, 0, state->utc_offset * 3600);
weekday = watch_utility_get_iso8601_weekday_number(scratch_time.unit.year, scratch_time.unit.month, scratch_time.unit.day) - 1;
// which planetary hour are we in?
planetary_hour = state->hour % 12;
// accomodate night hour count
if ( state->hour < 12 ) {
if ( state->start_at_night ) {
planetary_hour += 12;
}
} else {
if ( state->start_at_night ) {
weekday = ( weekday + 1 ) % 7;
} else {
planetary_hour += 12;
}
}
// make datetime object for selected planetary hour
scratch_time = watch_utility_date_time_from_unix_time(state->planetary_hours[state->hour], 0);
scratch_time = watch_utility_date_time_convert_zone(scratch_time, 0, state->utc_offset * 3600);
// round minutes
if (scratch_time.unit.second < 30 && scratch_time.unit.minute > 0 ) scratch_time.unit.minute--;
else if ( scratch_time.unit.minute < 59 ) scratch_time.unit.minute++;
// if we are in 12 hour mode, do some cleanup
if (!movement_clock_mode_24h()) {
if (scratch_time.unit.hour < 12) {
watch_clear_indicator(WATCH_INDICATOR_PM);
} else {
watch_set_indicator(WATCH_INDICATOR_PM);
}
scratch_time.unit.hour %= 12;
if (scratch_time.unit.hour == 0) scratch_time.unit.hour = 12;
}
// planetary ruler of the hour
planet = ( plindex[weekday] + planetary_hour ) % 7;
// latin or greek ruler names or astrological symbol
if ( state->ruler == 0 ) strncpy(ruler, planets[planet], 3);
if ( state->ruler == 1 ) strncpy(ruler, planetes[planet], 3);
if ( state->ruler == 2 ) strncpy(ruler, " ", 3);
// display planetary time with ruler of the hour or ruler of the day
sprintf(buf, "%s%2d%2d%02d ", ruler, (planetary_hour % 24) + 1, scratch_time.unit.hour, scratch_time.unit.minute);
watch_set_colon();
watch_display_string(buf, 0);
if ( state->ruler == 2 ) _planetary_icon(planet);
}
// PUBLIC WATCH FACE FUNCTIONS ////////////////////////////////////////////////
void planetary_hours_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(planetary_hours_state_t));
memset(*context_ptr, 0, sizeof(planetary_hours_state_t));
}
}
void planetary_hours_face_activate(void *context) {
if (watch_sleep_animation_is_running()) watch_stop_sleep_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
planetary_hours_state_t *state = (planetary_hours_state_t *)context;
_planetary_solar_phases(state);
}
bool planetary_hours_face_loop(movement_event_t event, void *context) {
planetary_hours_state_t *state = (planetary_hours_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
// Show your initial UI here.
watch_clear_indicator(WATCH_INDICATOR_PM);
watch_clear_indicator(WATCH_INDICATOR_24H);
_planetary_hours(state);
break;
case EVENT_LIGHT_BUTTON_UP:
state->ruler = (state->ruler + 1) % 3;
_planetary_hours(state);
break;
case EVENT_LIGHT_LONG_PRESS:
state->skip_to_current = true;
_planetary_hours(state);
break;
case EVENT_ALARM_BUTTON_UP:
state->hour++;
_planetary_hours(state);
break;
case EVENT_ALARM_LONG_PRESS:
state->hour--;
_planetary_hours(state);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void planetary_hours_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,109 @@
/*
* MIT License
*
* Copyright (c) 2023 Tobias Raayoni Last / @randogoth
* Copyright (c) 2022 Joey Castillo (sunrise_sunset_face)
*
* 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 planetary_hours_face_H_
#define planetary_hours_face_H_
/*
* PLANETARY HOURS face
*
* Background
*
* Both the 24 hour day and the order of our weekdays have quite esoteric roots.
* The ancient Egyptians divided the day up into 12 hours of sunlight and 12 hours
* of night time. Obviously the length of these hours varied throughout the year.
*
* The Greeks assigned each hour a ruler of the planetary gods in the ancient
* "Chaldean" order from slowest (Chronos for Saturn) to fastest (Selene for Moon).
* Because 24 hours cannot be equally divided by seven, the planetary rulers carried
* over to the first hour of the next day, effectively ruling over the entire day
* and lending the whole day their name. The seven day week was born.
*
* PLANETARY HOUR CHART COMPLICATION
*
* This complication watch face displays the start time of the current planetary hour
* according to the given location and day of the year. The number of the current
* planetary hour (1 - 24) is indicated at the top right.
*
* Short pressing the ALARM button flips through the start times of the following
* planetary hours, long pressing it flips backwards in time. A long press of the
* LIGHT button immediately switches back to the start time of the current hour.
* The Bell indicator always marks the current planetary hour in the list.
* The LAP indicator shows up when the hours of the next phase are part of the
* upcoming day instead of the current one. This happens when the watch face is
* launched after sunset.
*
* The planetary ruler of the current hour and day is displayed at the top in
* Latin or Greek shorthand notation:
*
* Saturn (SA) / Chronos (CH) / ♄
* Jupiter (JU) / Zeus (ZE) / ♃
* Mars (MA) / Ares (AR) / ♂
* Sol (SO) / Helios (HE) / ☉
* Venus (VE) / Aphrodite (AF) / ♀
* Mercury (ME) / Hermes (HR) / ☿
* Luna (LU) / Selene (SE) / ☾
*
* A short press of the LIGHT button toggles between Latin and Greek ruler shorthand
* notation.
*
* (IMPORTANT: Make sure the watch's time, timezone and location are set correctly for this
* watch face to work properly!)
*/
#include "movement.h"
#include "sunrise_sunset_face.h"
typedef struct {
// Anything you need to keep track of, put it here!
uint32_t planetary_hours[24];
uint32_t phase_start;
uint32_t phase_end;
uint32_t phase_next;
bool next;
double utc_offset;
bool no_location;
int8_t hour;
int8_t ruler;
bool start_at_night;
bool skip_to_current;
sunrise_sunset_state_t sunstate;
} planetary_hours_state_t;
void planetary_hours_face_setup(uint8_t watch_face_index, void ** context_ptr);
void planetary_hours_face_activate(void *context);
bool planetary_hours_face_loop(movement_event_t event, void *context);
void planetary_hours_face_resign(void *context);
#define planetary_hours_face ((const watch_face_t){ \
planetary_hours_face_setup, \
planetary_hours_face_activate, \
planetary_hours_face_loop, \
planetary_hours_face_resign, \
NULL, \
})
#endif // planetary_hours_face_H_

View File

@@ -0,0 +1,335 @@
/*
* MIT License
*
* Copyright (c) 2023 Tobias Raayoni Last / @randogoth
*
* 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 <math.h>
#include "sunriset.h"
#include "watch.h"
#include "watch_utility.h"
#include "planetary_time_face.h"
#if __EMSCRIPTEN__
#include <emscripten.h>
#endif
// STATIC FUNCTIONS AND CONSTANTS /////////////////////////////////////////////
/** @brief Planetary rulers in the Chaldean order from slowest to fastest
* @details Planetary rulers in the Chaldean order from slowest to fastest:
* Jupiter, Mars, Sun, Venus, Mercury, Moon
*/
static const char planets[7][3] = {"Sa", "Ju", "Ma", "So", "Ve", "Me", "Lu"}; // Latin
static const char planetes[7][3] = {"Ch", "Ze", "Ar", "He", "Af", "Hr", "Se"}; // Greek
/** @brief Ruler of each weekday for easy lookup
*/
static const uint8_t plindex[7] = {3, 6, 2, 5, 1, 4, 0}; // day ruler index
/** @brief Astrological symbol for each planet
*/
static void _planetary_icon(uint8_t planet) {
watch_clear_pixel(0, 13);
watch_clear_pixel(0, 14);
watch_clear_pixel(1, 13);
watch_clear_pixel(1, 14);
watch_clear_pixel(1, 15);
watch_clear_pixel(2, 13);
watch_clear_pixel(2, 14);
watch_clear_pixel(2, 15);
switch (planet) {
case 0: // Saturn
watch_set_pixel(0, 14);
watch_set_pixel(2, 14);
watch_set_pixel(1, 15);
watch_set_pixel(2, 13);
break;
case 1: // Jupiter
watch_set_pixel(0, 14);
watch_set_pixel(1, 15);
watch_set_pixel(1, 14);
break;
case 2: // Mars
watch_set_pixel(2, 14);
watch_set_pixel(2, 15);
watch_set_pixel(1, 15);
watch_set_pixel(2, 13);
watch_set_pixel(1, 13);\
break;
case 3: // Sol
watch_set_pixel(0, 14);
watch_set_pixel(2, 14);
watch_set_pixel(1, 13);
watch_set_pixel(2, 13);
watch_set_pixel(0, 13);
watch_set_pixel(2, 15);
break;
case 4: // Venus
watch_set_pixel(0, 14);
watch_set_pixel(0, 13);
watch_set_pixel(1, 13);
watch_set_pixel(1, 15);
watch_set_pixel(1, 14);
break;
case 5: // Mercury
watch_set_pixel(0, 14);
watch_set_pixel(1, 13);
watch_set_pixel(1, 14);
watch_set_pixel(1, 15);
watch_set_pixel(2, 15);
break;
case 6: // Luna
watch_set_pixel(2, 14);
watch_set_pixel(2, 15);
watch_set_pixel(2, 13);
break;
}
}
/** @details solar phase can be a day phase between sunrise and sunset or an alternating night phase.
* This function calculates the start and end of the current phase based on a given geographic location.
*/
static void _planetary_solar_phase(planetary_time_state_t *state) {
uint8_t phase;
double sunrise, sunset;
uint32_t now_epoch, sunrise_epoch, sunset_epoch, midnight_epoch;
movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1);
// check if we have a location. If not, display error
if (movement_location.reg == 0) {
watch_display_string(" no Loc", 0);
state->no_location = true;
return;
}
// location detected
state->no_location = false;
watch_date_time_t date_time = watch_rtc_get_date_time(); // the current local date / time
watch_date_time_t utc_now = watch_utility_date_time_convert_zone(date_time, movement_get_current_timezone_offset(), 0); // the current date / time in UTC
watch_date_time_t scratch_time; // scratchpad, contains different values at different times
watch_date_time_t midnight;
scratch_time.reg = midnight.reg = utc_now.reg;
midnight.unit.hour = midnight.unit.minute = midnight.unit.second = 0; // start of the day at midnight
// get location coordinate
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;
// save UTC offset
state->utc_offset = ((double)movement_get_current_timezone_offset()) / 3600.0;
// get UNIX epoch time
now_epoch = watch_utility_date_time_to_unix_time(utc_now, 0);
midnight_epoch = watch_utility_date_time_to_unix_time(midnight, 0);
// calculate sunrise and sunset of current day in decimal hours after midnight
sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &sunrise, &sunset);
// calculate sunrise and sunset UNIX timestamps
sunrise_epoch = midnight_epoch + sunrise * 3600;
sunset_epoch = midnight_epoch + sunset * 3600;
// by default we assume it is daytime (phase 1) between sunrise and sunset
phase = 1;
state->night = false;
state->phase_start = sunrise_epoch;
state->phase_end = sunset_epoch;
// night time calculations
if ( now_epoch < sunrise_epoch && now_epoch < sunset_epoch ) phase = 0; // morning before dawn
if ( now_epoch > sunrise_epoch && now_epoch >= sunset_epoch ) phase = 2; // evening after dusk
// phase 0: we are before sunrise
if ( phase == 0) {
// go back to yesterday and calculate sunset
midnight_epoch -= 86400;
scratch_time = watch_utility_date_time_from_unix_time(midnight_epoch, 0);
sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &sunrise, &sunset);
sunset_epoch = midnight_epoch + sunset * 3600;
// we are still in yesterday's night hours
state->night = true;
state->phase_start = sunset_epoch;
state->phase_end = sunrise_epoch;
}
// phase 2: we are after sunset
if ( phase == 2) {
// skip to tomorrow and calculate sunrise
midnight_epoch += 86400;
scratch_time = watch_utility_date_time_from_unix_time(midnight_epoch, 0);
sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &sunrise, &sunset);
sunrise_epoch = midnight_epoch + sunrise * 3600;
// we are still in yesterday's night hours
state->night = true;
state->phase_start = sunset_epoch;
state->phase_end = sunrise_epoch;
}
// calculate the duration of a planetary second during this solar phase
// and convert to Hertz so we can call a faster tick rate
state->freq = (1 / ((double)( state->phase_end - state->phase_start ) / 43200));
}
/** @details A planetary hour is one of exactly twelve hours of a solar phase. Its length varies.
* This function calculates the current planetary hour and divides it up into relative minutes and seconds.
* It also calculates the current planetary ruler of the hour and of the day.
*/
static void _planetary_time(movement_event_t event, planetary_time_state_t *state) {
char buf[14];
char ruler[3];
double night_hour_count = 0.0;
uint8_t weekday, planet, planetary_hour;
double hour_duration, current_hour, current_minute, current_second;
watch_set_colon();
// get current time and convert to UTC
state->scratch = watch_utility_date_time_convert_zone(watch_rtc_get_date_time(), movement_get_current_timezone_offset(), 0);
// when current phase ends calculate the next phase
if ( watch_utility_date_time_to_unix_time(state->scratch, 0) >= state->phase_end ) {
_planetary_solar_phase(state);
return;
}
if (movement_clock_mode_24h()) watch_set_indicator(WATCH_INDICATOR_24H);
// PM for night hours, otherwise the night hours are counted from 13
if ( state->night ) {
if (movement_clock_mode_24h()) night_hour_count = 12;
else watch_set_indicator(WATCH_INDICATOR_PM);
}
// calculate the duration of a planetary hour during this solar phase
hour_duration = (( state->phase_end - state->phase_start)) / 12.0;
// which planetary hour are we in?
// RTC only provides full second precision, so we have to manually add subseconds with each tick
current_hour = ((( watch_utility_date_time_to_unix_time(state->scratch, 0) ) + event.subsecond * 0.11111111) - state->phase_start ) / hour_duration;
planetary_hour = floor(current_hour) + ( state->night ? 12 : 0 );
current_hour += night_hour_count; //adjust for 24hr display
current_minute = modf(current_hour, &current_hour) * 60.0;
current_second = modf(current_minute, &current_minute) * 60.0;
// the day changes after sunrise, so if we are at night it is yesterday's planetary day
// hence we take the datetime object of when the last solar phase started as the current day
// and then fill in the hours and minutes
state->scratch = watch_utility_date_time_from_unix_time(state->phase_start, 0);
state->scratch.unit.hour = floor(current_hour);
state->scratch.unit.minute = floor(current_minute);
state->scratch.unit.second = (uint8_t)floor(current_second) % 60;
// what weekday is it (0 - 6)
weekday = watch_utility_get_iso8601_weekday_number(state->scratch.unit.year, state->scratch.unit.month, state->scratch.unit.day) - 1;
// planetary ruler of the hour or the day
if ( state->day_ruler ) planet = plindex[weekday];
else planet = ( plindex[weekday] + planetary_hour ) % 7;
// latin or greek ruler names or astrological symbol
if ( state->ruler == 0 ) strncpy(ruler, planets[planet], 3);
if ( state->ruler == 1 ) strncpy(ruler, planetes[planet], 3);
if ( state->ruler == 2 ) strncpy(ruler, " ", 3);
// display planetary time with ruler of the hour or ruler of the day
if ( state->day_ruler ) sprintf(buf, "%s d%2d%02d%02d", ruler, state->scratch.unit.hour, state->scratch.unit.minute, state->scratch.unit.second);
else sprintf(buf, "%s h%2d%02d%02d", ruler, state->scratch.unit.hour, state->scratch.unit.minute, state->scratch.unit.second);
watch_display_string(buf, 0);
if ( state->ruler == 2 ) _planetary_icon(planet);
}
// PUBLIC WATCH FACE FUNCTIONS ////////////////////////////////////////////////
void planetary_time_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(planetary_time_state_t));
memset(*context_ptr, 0, sizeof(planetary_time_state_t));
}
}
void planetary_time_face_activate(void *context) {
if (watch_sleep_animation_is_running()) watch_stop_sleep_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
planetary_time_state_t *state = (planetary_time_state_t *)context;
// calculate phase
_planetary_solar_phase(state);
}
bool planetary_time_face_loop(movement_event_t event, void *context) {
planetary_time_state_t *state = (planetary_time_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
_planetary_time(event, state);
if ( state->freq > 1 )
// for hours with shorter seconds let's increase the tick to not skip seconds in the display
movement_request_tick_frequency( 8 );
break;
case EVENT_TICK:
_planetary_time(event, state);
break;
case EVENT_LIGHT_BUTTON_UP:
state->ruler = (state->ruler + 1) % 3;
break;
case EVENT_ALARM_BUTTON_UP:
// Just in case you have need for another button.
state->day_ruler = !state->day_ruler;
break;
case EVENT_LOW_ENERGY_UPDATE:
watch_start_sleep_animation(500);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void planetary_time_face_resign(void *context) {
(void) context;
movement_request_tick_frequency( 1 );
}

View File

@@ -0,0 +1,110 @@
/*
* MIT License
*
* Copyright (c) 2023 Tobias Raayoni Last / @randogoth
* Copyright (c) 2022 Joey Castillo (sunrise_sunset_face)
*
* 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 planetary_time_face_H_
#define planetary_time_face_H_
/*
* PLANETARY TIME face
*
* BACKGROUND
*
* Both the 24 hour day and the order of our weekdays have quite esoteric roots.
* The ancient Egyptians divided the day up into 12 hours of sunlight and 12 hours
* of night time. Obviously the length of these hours varied throughout the year.
*
* The Greeks assigned each hour a ruler of the planetary gods in the ancient
* "Chaldean" order from slowest (Chronos for Saturn) to fastest (Selene for Moon).
* Because 24 hours cannot be equally divided by seven, the planetary rulers carried
* over to the first hour of the next day, effectively ruling over the entire day
* and lending the whole day their name. The seven day week was born.
*
* PLANETARY TIME COMPLICATION
*
* The hour digits of this complication watch-face display the current planetary hour
* according to the given location and day of the year (First hour from 12am to 1am,
* the second hour from 1am to 2am, and so forth).
*
* Like with normal clocks the minutes and seconds help dividing the hour into smaller
* units. On this watch-face, all units naturally vary in length because the planetary
* hours are not fixed by duration but by the moments of sunrise and sunset which
* obviously vary throughout the year, especially in higher latitudes.
*
* On this watch-face the hours indicated as 12am to 12pm (00:00 - 12:00) are used for
* the planetary daytime hours between sunrise and sunset and hours indicated as 12pm
* to 12am (12:00 - 00:00) are used for the planetary night hours after sunset and before
* sunrise.
*
* The planetary ruler of the current hour and day is displayed at the top in Latin or
* Greek shorthand notation:
*
* Saturn (SA) / Chronos (CH) / ♄
* Jupiter (JU) / Zeus (ZE) / ♃
* Mars (MA) / Ares (AR) / ♂
* Sol (SO) / Helios (HE) / ☉
* Venus (VE) / Aphrodite (AF) / ♀
* Mercury (ME) / Hermes (HR) / ☿
* Luna (LU) / Selene (SE) / ☾
*
* The ALARM button toggles between displaying the ruler of the hour and the ruler of the day
*
* The LIGHT button toggles between Latin and Greek ruler shorthand notation
*
* (IMPORTANT: Make sure the watch's time, timezone and location are set correctly for this
* watch face to work properly!)
*/
#include "movement.h"
#include "sunrise_sunset_face.h"
typedef struct {
// Anything you need to keep track of, put it here!
uint32_t phase_start;
uint32_t phase_end;
bool night;
double utc_offset;
double freq;
uint8_t ruler;
bool day_ruler;
bool no_location;
sunrise_sunset_state_t sunstate;
watch_date_time_t scratch;
} planetary_time_state_t;
void planetary_time_face_setup(uint8_t watch_face_index, void ** context_ptr);
void planetary_time_face_activate(void *context);
bool planetary_time_face_loop(movement_event_t event, void *context);
void planetary_time_face_resign(void *context);
#define planetary_time_face ((const watch_face_t){ \
planetary_time_face_setup, \
planetary_time_face_activate, \
planetary_time_face_loop, \
planetary_time_face_resign, \
NULL, \
})
#endif // planetary_time_face_H_

View File

@@ -0,0 +1,176 @@
/*
* MIT License
*
* Copyright (c) 2022 Spencer Bywater
*
* 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 "probability_face.h"
#define DEFAULT_DICE_SIDES 2
#define PROBABILITY_ANIMATION_TICK_FREQUENCY 8
const uint16_t NUM_DICE_TYPES = 8; // Keep this consistent with # of dice types below
const uint16_t DICE_TYPES[] = {2, 4, 6, 8, 10, 12, 20, 100};
// --------------
// Custom methods
// --------------
static void display_dice_roll(probability_state_t *state) {
char buf[8];
if (state->rolled_value == 0) {
if (state->dice_sides == 100) {
sprintf(buf, " C ");
} else {
sprintf(buf, "%2d ", state->dice_sides);
}
} else if (state->dice_sides == 2) {
if (state->rolled_value == 1) {
sprintf(buf, "%2d H", state->dice_sides);
} else {
sprintf(buf, "%2d T", state->dice_sides);
}
} else if (state->dice_sides == 100) {
sprintf(buf, " C %3d", state->rolled_value);
} else {
sprintf(buf, "%2d %3d", state->dice_sides, state->rolled_value);
}
watch_display_string(buf, 4);
}
static void generate_random_number(probability_state_t *state) {
// Emulator: use rand. Hardware: use arc4random.
#if __EMSCRIPTEN__
state->rolled_value = rand() % state->dice_sides + 1;
#else
state->rolled_value = arc4random_uniform(state->dice_sides) + 1;
#endif
}
static void display_dice_roll_animation(probability_state_t *state) {
if (state->is_rolling) {
if (state->animation_frame == 0) {
watch_display_string(" ", 7);
watch_set_pixel(1, 4);
watch_set_pixel(1, 6);
state->animation_frame = 1;
} else if (state->animation_frame == 1) {
watch_clear_pixel(1, 4);
watch_clear_pixel(1, 6);
watch_set_pixel(2, 4);
watch_set_pixel(0, 6);
state->animation_frame = 2;
} else if (state->animation_frame == 2) {
watch_clear_pixel(2, 4);
watch_clear_pixel(0, 6);
watch_set_pixel(2, 5);
watch_set_pixel(0, 5);
state->animation_frame = 3;
} else if (state->animation_frame == 3) {
state->animation_frame = 0;
state->is_rolling = false;
movement_request_tick_frequency(1);
display_dice_roll(state);
}
}
}
// ---------------------------
// Standard watch face methods
// ---------------------------
void probability_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(probability_state_t));
memset(*context_ptr, 0, sizeof(probability_state_t));
}
// Emulator only: Seed random number generator
#if __EMSCRIPTEN__
srand(time(NULL));
#endif
}
void probability_face_activate(void *context) {
probability_state_t *state = (probability_state_t *)context;
state->dice_sides = DEFAULT_DICE_SIDES;
state->rolled_value = 0;
watch_display_string("PR", 0);
}
bool probability_face_loop(movement_event_t event, void *context) {
probability_state_t *state = (probability_state_t *)context;
if (state->is_rolling && event.event_type != EVENT_TICK) {
return true;
}
switch (event.event_type) {
case EVENT_ACTIVATE:
display_dice_roll(state);
break;
case EVENT_TICK:
display_dice_roll_animation(state);
break;
case EVENT_LIGHT_BUTTON_DOWN:
// Change how many sides the die has
for (int i = 0; i < NUM_DICE_TYPES; i++) {
if (DICE_TYPES[i] == state->dice_sides) {
if (i == NUM_DICE_TYPES - 1) {
state->dice_sides = DICE_TYPES[0];
} else {
state->dice_sides = DICE_TYPES[i + 1];
}
break;
}
}
state->rolled_value = 0;
display_dice_roll(state);
break;
case EVENT_ALARM_BUTTON_UP:
// Roll the die
generate_random_number(state);
state->is_rolling = true;
// Dice rolling animation begins on next tick and new roll will be displayed on completion
movement_request_tick_frequency(PROBABILITY_ANIMATION_TICK_FREQUENCY);
break;
case EVENT_LOW_ENERGY_UPDATE:
watch_display_string("SLEEP ", 4);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void probability_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,63 @@
/*
* MIT License
*
* Copyright (c) 2022 Spencer Bywater
*
* 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 PROBABILITY_FACE_H_
#define PROBABILITY_FACE_H_
/*
* PROBABILITY face
*
* This face is a dice-rolling random number generator.
* Supports dice with 2, 4, 6, 8, 10, 12, 20, or 100 sides.
*
* Press LIGHT to cycle through die type.
* The current die size is indicated on the left ("C" for 100)
*
* Press ALARM to roll the selected die.
*/
#include "movement.h"
typedef struct {
uint8_t dice_sides;
uint8_t rolled_value;
uint8_t animation_frame;
bool is_rolling;
} probability_state_t;
void probability_face_setup(uint8_t watch_face_index, void ** context_ptr);
void probability_face_activate(void *context);
bool probability_face_loop(movement_event_t event, void *context);
void probability_face_resign(void *context);
#define probability_face ((const watch_face_t){ \
probability_face_setup, \
probability_face_activate, \
probability_face_loop, \
probability_face_resign, \
NULL, \
})
#endif // PROBABILITY_FACE_H_

View File

@@ -0,0 +1,199 @@
/* SPDX-License-Identifier: MIT */
/*
* MIT License
*
* Copyright © 2021-2022 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com>
* Copyright © 2023 Jeremy O'Brien <neutral@fastmail.com>
* Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.com> (https://www.matheusmoreira.com/)
*
* 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 "pulsometer_face.h"
#include "watch.h"
#ifndef PULSOMETER_FACE_TITLE
#define PULSOMETER_FACE_TITLE "PL"
#endif
#ifndef PULSOMETER_FACE_CALIBRATION_DEFAULT
#define PULSOMETER_FACE_CALIBRATION_DEFAULT (30)
#endif
#ifndef PULSOMETER_FACE_CALIBRATION_INCREMENT
#define PULSOMETER_FACE_CALIBRATION_INCREMENT (10)
#endif
// tick frequency will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.)
#ifndef PULSOMETER_FACE_FREQUENCY_FACTOR
#define PULSOMETER_FACE_FREQUENCY_FACTOR (4ul)
#endif
#define PULSOMETER_FACE_FREQUENCY (1 << PULSOMETER_FACE_FREQUENCY_FACTOR)
typedef struct {
bool measuring;
int16_t pulses;
int16_t ticks;
int8_t calibration;
} pulsometer_state_t;
static void pulsometer_display_title(pulsometer_state_t *pulsometer) {
(void) pulsometer;
watch_display_string(PULSOMETER_FACE_TITLE, 0);
}
static void pulsometer_display_calibration(pulsometer_state_t *pulsometer) {
char buf[3];
snprintf(buf, sizeof(buf), "%2hhd", pulsometer->calibration);
watch_display_string(buf, 2);
}
static void pulsometer_display_measurement(pulsometer_state_t *pulsometer) {
char buf[7];
snprintf(buf, sizeof(buf), "%-6hd", pulsometer->pulses);
watch_display_string(buf, 4);
}
static void pulsometer_indicate(pulsometer_state_t *pulsometer) {
if (pulsometer->measuring) {
watch_set_indicator(WATCH_INDICATOR_LAP);
} else {
watch_clear_indicator(WATCH_INDICATOR_LAP);
}
}
static void pulsometer_start_measurement(pulsometer_state_t *pulsometer) {
pulsometer->measuring = true;
pulsometer->pulses = INT16_MAX;
pulsometer->ticks = 0;
pulsometer_indicate(pulsometer);
movement_request_tick_frequency(PULSOMETER_FACE_FREQUENCY);
}
static void pulsometer_measure(pulsometer_state_t *pulsometer) {
if (!pulsometer->measuring) { return; }
pulsometer->ticks++;
float ticks_per_minute = 60 << PULSOMETER_FACE_FREQUENCY_FACTOR;
float pulses_while_button_held = ticks_per_minute / pulsometer->ticks;
float calibrated_pulses = pulses_while_button_held * pulsometer->calibration;
calibrated_pulses += 0.5f;
pulsometer->pulses = (int16_t) calibrated_pulses;
pulsometer_display_measurement(pulsometer);
}
static void pulsometer_stop_measurement(pulsometer_state_t *pulsometer) {
movement_request_tick_frequency(1);
pulsometer->measuring = false;
pulsometer_display_measurement(pulsometer);
pulsometer_indicate(pulsometer);
}
static void pulsometer_cycle_calibration(pulsometer_state_t *pulsometer, int8_t increment) {
if (pulsometer->measuring) { return; }
if (pulsometer->calibration <= 0) {
pulsometer->calibration = 1;
}
int8_t last = pulsometer->calibration;
pulsometer->calibration += increment;
if (pulsometer->calibration > 39) {
pulsometer->calibration = last == 39? 1 : 39;
}
pulsometer_display_calibration(pulsometer);
}
void pulsometer_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
pulsometer_state_t *pulsometer = malloc(sizeof(pulsometer_state_t));
pulsometer->calibration = PULSOMETER_FACE_CALIBRATION_DEFAULT;
pulsometer->pulses = 0;
pulsometer->ticks = 0;
*context_ptr = pulsometer;
}
}
void pulsometer_face_activate(void *context) {
pulsometer_state_t *pulsometer = context;
pulsometer->measuring = false;
pulsometer_display_title(pulsometer);
pulsometer_display_calibration(pulsometer);
pulsometer_display_measurement(pulsometer);
}
bool pulsometer_face_loop(movement_event_t event, void *context) {
pulsometer_state_t *pulsometer = (pulsometer_state_t *) context;
switch (event.event_type) {
case EVENT_ALARM_BUTTON_DOWN:
pulsometer_start_measurement(pulsometer);
break;
case EVENT_ALARM_BUTTON_UP:
case EVENT_ALARM_LONG_UP:
pulsometer_stop_measurement(pulsometer);
break;
case EVENT_TICK:
pulsometer_measure(pulsometer);
break;
case EVENT_LIGHT_BUTTON_UP:
pulsometer_cycle_calibration(pulsometer, 1);
break;
case EVENT_LIGHT_LONG_UP:
pulsometer_cycle_calibration(pulsometer, PULSOMETER_FACE_CALIBRATION_INCREMENT);
break;
case EVENT_LIGHT_BUTTON_DOWN:
// Inhibit the LED
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void pulsometer_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,87 @@
/* SPDX-License-Identifier: MIT */
/*
* MIT License
*
* Copyright © 2021-2022 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com>
* Copyright © 2022 Alexsander Akers <me@a2.io>
* Copyright © 2023 Alex Utter <ooterness@gmail.com>
* Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.com> (https://www.matheusmoreira.com/)
*
* 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 PULSOMETER_FACE_H_
#define PULSOMETER_FACE_H_
/*
* PULSOMETER face
*
* The pulsometer implements a classic mechanical watch complication.
* A mechanical pulsometer involves a chronograph with a scale that
* allows the user to compute the number of heart beats per minute
* in less time. The scale is calibrated, or graduated, for a fixed
* number of heart beats, most often 30. The user starts the chronograph
* and simultaneously begins counting the heart beats. The movement of
* the chronograph's seconds hand over time automatically performs the
* computations required. When the calibrated number of heart beats
* is reached, the chronograph is stopped and the seconds hand shows
* the heart rate.
*
* The Sensor Watch pulsometer improves this design with user calibration:
* it can be graduated to any value between 1 and 39 pulsations per minute.
* The default is still 30, mirroring the classic pulsometer calibration.
* This feature allows the user to reconfigure the pulsometer to count
* many other types of periodic minutely events, making it more versatile.
* For example, it can be set to 5 respirations per minute to turn it into
* an asthmometer, a nearly identical mechanical watch complication
* that doctors might use to quickly measure respiratory rate.
*
* To use the pulsometer, hold the ALARM button and count the pulses.
* When the calibrated number of pulses is reached, release the button.
* The display will show the number of pulses per minute.
*
* In order to measure heart rate, feel for a pulse using the hand with
* the watch while holding the button down with the other.
* The pulse can be easily felt on the carotid artery of the neck.
*
* In order to measure breathing rate, simply hold the ALARM button
* and count the number of breaths.
*
* To calibrate the pulsometer, press LIGHT
* to cycle to the next integer calibration.
* Long press LIGHT to cycle it by 10.
*/
#include "movement.h"
void pulsometer_face_setup(uint8_t watch_face_index, void ** context_ptr);
void pulsometer_face_activate(void *context);
bool pulsometer_face_loop(movement_event_t event, void *context);
void pulsometer_face_resign(void *context);
#define pulsometer_face ((const watch_face_t){ \
pulsometer_face_setup, \
pulsometer_face_activate, \
pulsometer_face_loop, \
pulsometer_face_resign, \
NULL, \
})
#endif // PULSOMETER_FACE_H_

View File

@@ -0,0 +1,411 @@
/*
* MIT License
*
* Copyright (c) 2023 Tobias Raayoni Last / @randogoth
*
* 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>
#else
#include "saml22j18a.h"
#endif
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "filesystem.h"
#include "randonaut_face.h"
#define R 6371 // Earth's radius in km
#define PI 3.14159265358979323846
static void _get_location_from_file(randonaut_state_t *state);
static void _save_point_to_file(randonaut_state_t *state);
static void _get_entropy(randonaut_state_t *state);
static void _generate_blindspot(randonaut_state_t *state);
static void _randonaut_face_display(randonaut_state_t *state);
static void _generate_blindspot(randonaut_state_t *state);
static uint32_t _get_pseudo_entropy(uint32_t max);
static uint32_t _get_true_entropy(void);
static void _get_entropy(randonaut_state_t *state);
// MOVEMENT WATCH FACE FUNCTIONS //////////////////////////////////////////////
void randonaut_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(randonaut_state_t));
memset(*context_ptr, 0, sizeof(randonaut_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.
}
void randonaut_face_activate(void *context) {
randonaut_state_t *state = (randonaut_state_t *)context;
_get_location_from_file(state);
state->face.mode = 0;
state->radius = 1000;
_get_entropy(state);
state->chance = true;
// Handle any tasks related to your watch face coming on screen.
}
bool randonaut_face_loop(movement_event_t event, void *context) {
randonaut_state_t *state = (randonaut_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
// Show your initial UI here.
break;
case EVENT_TICK:
// If needed, update your display here.
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_BUTTON_UP:
switch ( state->face.mode ) {
case 0: // home
state->face.mode = 2; //point
state->face.location_format = 0; // title
break;
case 1: // generate
state->face.mode = 0; //home
break;
case 2: // point
state->face.mode = 0; //home
break;
case 3: // setup radius
state->face.mode = 4; // toggle to RNG
break;
case 4: // setup RNG
state->face.mode = 3; // toggle to Radius
break;
case 5: // data processing
break;
}
break;
case EVENT_LIGHT_LONG_PRESS:
switch ( state->face.mode ) {
case 3: // setup
case 4:
state->face.mode = 0; //home
break;
default:
state->face.mode = 3; //setup
watch_clear_display();
}
break;
case EVENT_ALARM_BUTTON_UP:
switch ( state->face.mode ) {
case 0: //home
state->face.mode = 1; // generate
break;
case 2: // point
state->face.location_format = (( state->face.location_format + 1) % (7));
if ( state->face.location_format == 0 )
state->face.location_format++;
break;
case 3: //setup radius
state->radius += 500;
if ( state->radius > 10000 )
state->radius = 1000;
break;
case 4: //setup RNG
state->face.rng = (state->face.rng + 1) % 3;
switch ( state->face.rng ) {
case 0:
state->chance = true;
break;
case 1:
state->chance = false;
state->quantum = true;
break;
case 2:
state->chance = false;
state->quantum = false;
break;
}
break;
case 5: // data processing
_save_point_to_file(state);
break;
default:
break;
}
break;
case EVENT_ALARM_LONG_PRESS:
if ( state->face.mode == 5 )
state->face.mode = 0; // home
else
state->face.mode = 5; // data processing
break;
case EVENT_TIMEOUT:
// Your watch face will receive this event after a period of inactivity. If it makes sense to resign,
// you may uncomment this line to move back to the first watch face in the list:
// 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_sleep_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);
}
_randonaut_face_display(state);
// return true if the watch can enter standby mode. Generally speaking, you should always return true.
// Exceptions:
// * If you are displaying a color using the low-level watch_set_led_color function, you should return false.
// * If you are sounding the buzzer using the low-level watch_set_buzzer_on function, you should return false.
// Note that if you are driving the LED or buzzer using Movement functions like movement_illuminate_led or
// movement_play_alarm, you can still return true. This guidance only applies to the low-level watch_ functions.
return true;
}
void randonaut_face_resign(void *context) {
(void) context;
// handle any cleanup before your watch face goes off-screen.
}
// PRIVATE STATIC FUNCTIONS ///////////////////////////////////////////////////
/** @brief display handler
*/
static void _randonaut_face_display(randonaut_state_t *state) {
char buf[12];
watch_clear_colon();
switch ( state->face.mode ) {
case 0: //home
sprintf(buf, "RA Rando");
break;
case 1: //generate
if ( state->quantum )
// All Hail Steve /;[;[/.;]/[.;[/;/;/;/;.;.];.]]--=/
for ( uint8_t c = 100; c > 0; c--) {
watch_set_pixel(_get_pseudo_entropy(0x2),_get_pseudo_entropy(0x33-0x1C));
watch_set_pixel(_get_pseudo_entropy(0x2),_get_pseudo_entropy(3432-3409));
watch_set_pixel(_get_pseudo_entropy(002),_get_pseudo_entropy(0xE +9));
watch_set_pixel(_get_pseudo_entropy(0x2),_get_pseudo_entropy(23));
watch_set_pixel(_get_pseudo_entropy(002),_get_pseudo_entropy(12+7+11));
if( c < 70 ) {
watch_clear_pixel(_get_pseudo_entropy(2),_get_pseudo_entropy(12+7+11));
}
if ( c < 60 ) {
watch_clear_pixel(_get_pseudo_entropy(002),_get_pseudo_entropy(0xD68-0xD4A));
}
if ( c < 50 ) {
watch_clear_pixel(_get_pseudo_entropy(0x2),_get_pseudo_entropy(14+9));
}
delay_ms(_get_pseudo_entropy(c)+20);
if ( c < 30 ) {
watch_display_string(" ",_get_pseudo_entropy(10));
}
watch_clear_pixel(_get_pseudo_entropy(02),_get_pseudo_entropy(3432-3409));
watch_clear_pixel(_get_pseudo_entropy(002),_get_pseudo_entropy(51-28));
watch_clear_pixel(_get_pseudo_entropy(0x2),_get_pseudo_entropy(23));
if ( c < 20 ) {
watch_clear_pixel(_get_pseudo_entropy(02),_get_pseudo_entropy(51-28));
watch_clear_pixel(_get_pseudo_entropy(2),_get_pseudo_entropy(14+9));
watch_clear_pixel(_get_pseudo_entropy(0x2),_get_pseudo_entropy(0xD68-0xD4A));
watch_clear_pixel(_get_pseudo_entropy(0x2),_get_pseudo_entropy(3432-3409));
watch_clear_pixel(_get_pseudo_entropy(002),_get_pseudo_entropy(12+7+11));
watch_clear_pixel(_get_pseudo_entropy(2),_get_pseudo_entropy(51-28));
}
}
else
for ( uint8_t c = 30; c > 0; c--) {
watch_display_string("1", _get_pseudo_entropy(10));
watch_display_string("0", _get_pseudo_entropy(10));
watch_display_string("11", _get_pseudo_entropy(10));
watch_display_string("00", _get_pseudo_entropy(10));
delay_ms(50);
watch_display_string(" ", _get_pseudo_entropy(10));
watch_display_string(" ", _get_pseudo_entropy(10));
watch_display_string(" ", _get_pseudo_entropy(10));
watch_display_string(" ", _get_pseudo_entropy(10));
}
_generate_blindspot(state);
watch_clear_display();
state->face.mode = 2; // point
state->face.location_format = 1; // distance
watch_display_string("RA Found", 0);
delay_ms(500);
sprintf(buf, "RA Found");
break;
case 2: //point
switch ( state->face.location_format ) {
case 0:
sprintf(buf, "RA Point");
break;
case 1: // distance to point
watch_clear_display();
sprintf(buf, "DI m %d", state->point.distance );
break;
case 2: // bearing relative to point
watch_clear_display();
sprintf(buf, "BE # %d", state->point.bearing );
break;
case 3: // latitude DD._____
sprintf(state->scratchpad, "%07d", abs((int32_t)(state->point.latitude)));
sprintf(buf, "LA #%c %c%c ", state->point.latitude < 0 ? '-' : '+', state->scratchpad[0], state->scratchpad[1]);
break;
case 4: // latitude __.DDDDD
sprintf(buf, "LA , %c%c%c%c%c", state->scratchpad[2], state->scratchpad[3],state->scratchpad[4], state->scratchpad[5],state->scratchpad[6]);
break;
case 5: // longitude DD._____
sprintf(state->scratchpad, "%08d", abs((int32_t)(state->point.longitude)));
sprintf(buf, "LO #%c%c%c%c ", state->point.longitude < 0 ? '-' : '+',state->scratchpad[0], state->scratchpad[1], state->scratchpad[2]);
break;
case 6: // longitude __.DDDDD
sprintf(buf, "LO , %c%c%c%c%c", state->scratchpad[3], state->scratchpad[4],state->scratchpad[5], state->scratchpad[6],state->scratchpad[7]);
break;
}
break;
case 3: // setup radius
watch_set_colon();
if ( state->radius < 10000 )
sprintf(buf, "RA m %d ", state->radius);
else
sprintf(buf, "RA m%d ", state->radius);
break;
case 4: // setup RNG
sprintf(buf, "RN G %s ", state->chance ? "Chnce" : (state->quantum ? "True" : "Psudo"));
break;
case 5: // data processing
sprintf(buf, "WR File ");
}
watch_display_string(buf, 0);
}
/** @brief Official Randonautica Blindspot Algorithm
*/
static void _generate_blindspot(randonaut_state_t *state) {
_get_entropy(state);
double lat = (double)state->location.latitude / 100000;
double lon = (double)state->location.longitude / 100000;
uint16_t radius = state->radius;
const double random_distance = radius * sqrt( (double)state->entropy / INT32_MAX ) / 1000.0;
const double random_bearing = 2.0 * PI * (double)state->entropy / INT32_MAX;
const double phi = lat * PI / 180;
const double lambda = lon * PI / 180;
const double alpha = random_distance / R;
lat = asin( sin(phi) * cos(alpha) + cos(phi) * sin(alpha) * cos(random_bearing) );
lon = lambda + atan2( sin(random_bearing) * sin(alpha) * cos(phi), cos(alpha) - sin(phi) * sin( lat ));
state->point.latitude = (int)round(lat * (180 / PI) * 100000);
state->point.longitude = (int)round(lon * (180 / PI) * 100000);
state->point.distance = random_distance * 1000;
state->point.bearing = (uint16_t)round(random_bearing * (180 / PI) < 0 ? random_bearing * (180 / PI) + 360 : random_bearing * (180 / PI));
}
/** @brief pseudo random number generator
*/
static uint32_t _get_pseudo_entropy(uint32_t max) {
#if __EMSCRIPTEN__
return rand() % max;
#else
return arc4random_uniform(max);
#endif
}
/** @brief true random number generator
*/
static uint32_t _get_true_entropy(void) {
#if __EMSCRIPTEN__
return rand() % INT32_MAX;
#else
hri_mclk_set_APBCMASK_TRNG_bit(MCLK);
hri_trng_set_CTRLA_ENABLE_bit(TRNG);
while (!hri_trng_get_INTFLAG_reg(TRNG, TRNG_INTFLAG_DATARDY)); // Wait for TRNG data to be ready
watch_disable_TRNG();
hri_mclk_clear_APBCMASK_TRNG_bit(MCLK);
return hri_trng_read_DATA_reg(TRNG); // Read a single 32-bit word from TRNG and return it
#endif
}
/** @brief get location from place.loc
*/
static void _get_location_from_file(randonaut_state_t *state) {
movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1);
coordinate_t place;
if (filesystem_file_exists("place.loc")) {
if (filesystem_read_file("place.loc", (char*)&place, sizeof(place)))
state->location = place;
} else {
watch_set_indicator(WATCH_INDICATOR_BELL);
state->location.latitude = movement_location.bit.latitude * 1000;
state->location.longitude = movement_location.bit.longitude * 1000;
}
}
/** @brief save generated point to place.loc
*/
static void _save_point_to_file(randonaut_state_t *state) {
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
coordinate_t place;
place.latitude = state->point.latitude;
place.longitude = state->point.longitude;
if (filesystem_write_file("place.loc", (char*)&place, sizeof(place))) {
delay_ms(100);
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
} else {
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
watch_set_indicator(WATCH_INDICATOR_BELL);
delay_ms(500);
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
}
/** @brief get pseudo/quantum entropy and filter modulo bias
*/
static void _get_entropy(randonaut_state_t *state) {
if ( state->chance ) {
state->quantum = (bool)(state->entropy % 2);
}
do {
if ( !state->quantum ) {
state->entropy = _get_pseudo_entropy(INT32_MAX);
} else {
state->entropy = _get_true_entropy();
}
} while (state->entropy >= INT32_MAX || state->entropy <= 0);
state->entropy %= INT32_MAX;
}

View File

@@ -0,0 +1,113 @@
/*
* MIT License
*
* Copyright (c) 2023 Tobias Raayoni Last / @randogoth
*
* 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 RANDONAUT_FACE_H_
#define RANDONAUT_FACE_H_
/*
* RANDONAUT face
* ==============
*
* Randonauting is a way to turn the world around you into an adventure and get the user outside
* of their day-to-day routine by using a random number generator to derive a coordinate to journey
* to. In Randonauts lore so-called "Blind Spots" are places you cannot reach methodologically. They
* may exist in your own backyard for your whole life and you will never even notice them, because
* you simply have no reason to go to that exact place or look in its direction. Since the very
* limitations of our behavioral algorithms are the reason for the existence of blindspots, they
* can only be found using a randomizer.
*
* This watch face generates a random location based on the watch's location and a set radius using
* the official Randonautica Blind Spot algorithm.
*
* The ALARM button starts the random location generation and then automatically displays the found
* Blind Spot.
*
* By pressing ALARM again the user can flip through different pieces of information about the Blind
* Spot: Distance (DI), Bearing Degree (BE), Latitude degrees and decimal digits (LA), Longitude
* degrees and decimal digits (LO).
*
* Pressing LIGHT switches between generating a new blind spot ("Rando") and displaying the info of
* the last generated one ("Point").
*
* LONG PRESSING LIGHT toggles setup mode. Here pressing LIGHT switches between setting the desired
* radius (RA) and setting the random number generator (RNG) for generating the blind spot.
*
* ALARM changes the values respectively:
*
* - The radius can be set in 500 meter steps between 1000 and 10,000 meters
*
* - The RNG can be set to "true" which utilizes the SAML22J's internal True Random Number Generator
* - Setting it to "psudo" will use the pseudorandom number generation algorithm arc4random
* - Setting it to "chance" will randomly chose either of the RNGs for each generation (default)
*
* LONG PRESSING ALARM toggles DATA mode in which the currently generated Blind Spot coordinate can
* be written to the <place.loc> file on the watch (press ALARM) and set as active high precision
* location used by other watch faces. It does not overwrite the low precision location information
* in the watch register commonly used for astronomical watch faces.
*
*/
#include "movement.h"
#include "place_face.h"
typedef struct {
uint8_t mode :3;
uint8_t location_format :3;
uint8_t rng: 2;
} randonaut_face_mode_t;
typedef struct {
int32_t latitude : 26;
int32_t longitude : 26;
uint16_t distance : 14;
uint16_t bearing : 9;
} randonaut_coordinate_t;
typedef struct {
// Anything you need to keep track of, put it here!
coordinate_t location;
randonaut_coordinate_t point;
uint16_t radius : 14;
uint32_t entropy;
bool quantum;
bool chance;
randonaut_face_mode_t face;
char scratchpad[10];
} randonaut_state_t;
void randonaut_face_setup(uint8_t watch_face_index, void ** context_ptr);
void randonaut_face_activate(void *context);
bool randonaut_face_loop(movement_event_t event, void *context);
void randonaut_face_resign(void *context);
#define randonaut_face ((const watch_face_t){ \
randonaut_face_setup, \
randonaut_face_activate, \
randonaut_face_loop, \
randonaut_face_resign, \
NULL, \
})
#endif // RANDONAUT_FACE_H_

View File

@@ -0,0 +1,88 @@
/*
* MIT License
*
* Copyright (c) 2022 David Singleton
*
* 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 "ratemeter_face.h"
#include "watch.h"
#define RATEMETER_FACE_FREQUENCY_FACTOR (4ul) // refresh rate will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.)
#define RATEMETER_FACE_FREQUENCY (1 << RATEMETER_FACE_FREQUENCY_FACTOR)
void ratemeter_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) *context_ptr = malloc(sizeof(ratemeter_state_t));
}
void ratemeter_face_activate(void *context) {
memset(context, 0, sizeof(ratemeter_state_t));
}
bool ratemeter_face_loop(movement_event_t event, void *context) {
ratemeter_state_t *ratemeter_state = (ratemeter_state_t *)context;
char buf[14];
switch (event.event_type) {
case EVENT_ACTIVATE:
watch_display_string("ra ", 0);
break;
case EVENT_ALARM_BUTTON_DOWN:
if (ratemeter_state->ticks != 0) {
ratemeter_state->rate = (int16_t)(60.0 / ((float)ratemeter_state->ticks / (float)RATEMETER_FACE_FREQUENCY));
}
ratemeter_state->ticks = 0;
movement_request_tick_frequency(RATEMETER_FACE_FREQUENCY);
break;
case EVENT_ALARM_BUTTON_UP:
break;
case EVENT_ALARM_LONG_PRESS:
break;
case EVENT_TICK:
if (ratemeter_state->rate == 0) {
watch_display_string("ra ", 0);
} else {
if (ratemeter_state->rate > 500) {
watch_display_string("ra Hi", 0);
} else if (ratemeter_state->rate < 1) {
watch_display_string("ra Lo", 0);
} else {
sprintf(buf, "ra %-3d pn", ratemeter_state->rate);
watch_display_string(buf, 0);
}
}
ratemeter_state->ticks++;
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void ratemeter_face_resign(void *context) {
(void) context;
}

View 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.
*/
#ifndef RATEMETER_FACE_H_
#define RATEMETER_FACE_H_
/*
* RATE METER face
*
* The rate meter shows the rate per minute at which the ALARM button is
* being pressed. This is particularly useful in sports where cadence
* tracking is useful. For instance, rowing coaches often use a dedicated
* rate meter - clicking the rate button each time the crew puts their oars
* in the water to see the rate (strokes per minute) on the rate meter.
*/
#include "movement.h"
typedef struct {
int16_t rate;
int16_t ticks;
} ratemeter_state_t;
void ratemeter_face_setup(uint8_t watch_face_index, void ** context_ptr);
void ratemeter_face_activate(void *context);
bool ratemeter_face_loop(movement_event_t event, void *context);
void ratemeter_face_resign(void *context);
#define ratemeter_face ((const watch_face_t){ \
ratemeter_face_setup, \
ratemeter_face_activate, \
ratemeter_face_loop, \
ratemeter_face_resign, \
NULL, \
})
#endif // RATEMETER_FACE_H_

View File

@@ -0,0 +1,424 @@
/*
* MIT License
*
* Copyright (c) 2022 James Haggerty
*
* 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 <math.h>
#include "rpn_calculator_alt_face.h"
static void show_fn(calculator_state_t *state, uint8_t subsecond);
void rpn_calculator_alt_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(calculator_state_t));
memset(*context_ptr, 0, sizeof(calculator_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.
}
static void show_number(double num) {
char buf[9] = {0};
bool negative = num < 0;
int max_digits = negative ? 5 : 6;
// Add back in for debugging...
// printf("%f\n", num);
if (isnan(num)) {
watch_clear_colon();
watch_display_string(" nan ", 2);
return;
}
if (negative) {
num = -num;
}
// Can we reasonably represent this number without a decimal point?
if (num == 0 || (num >= 0.5 && fabs(num - (int)num) < 0.0001)) {
if (floor(log10(num)) + 1 <= max_digits) {
if (negative) {
sprintf(buf, " -%-5d", (int)round(num));
} else {
sprintf(buf, " %-6d", (int)round(num));
}
watch_clear_colon();
watch_display_string(buf, 2);
return;
}
}
// Is this a floating point number where scientific
// notation won't get us much? (i.e. between 0.1 and 1)
if (num < 1 && num >= 0.0999) {
// Display as boring floating point number... (e.g. 0.25)
int digits = (int)round(num * 10000);
sprintf(buf, " 0%04d", digits);
if (negative) {
buf[2 ] = '-';
}
watch_set_colon();
watch_display_string(buf, 2);
return;
}
// Fall back to scientific notation
// Calculate exponent
int exponent = 0;
while (num < 1) {
num *= 10;
--exponent;
}
while (num >= 10) {
num /= 10;
++exponent;
}
if (exponent < -9) {
sprintf(buf, " small ");
watch_clear_colon();
watch_display_string(buf, 2);
return;
}
if (exponent > 39) {
sprintf(buf, " big ");
watch_clear_colon();
watch_display_string(buf, 2);
return;
}
sprintf(buf, "%2d%c%05d", exponent, negative ? '-' : ' ', (int)round(num * 10000));
watch_set_colon();
watch_display_string(buf, 2);
}
#define C (s->stack[s->stack_size - 1])
#define PUSH(x) (s->stack[++s->stack_size - 1] = x)
#define POP() (s->stack[s->stack_size-- - 1])
void rpn_calculator_alt_face_activate(void *context) {
calculator_state_t *s = (calculator_state_t *)context;
s->min = s->max = NAN;
}
static void change_mode(calculator_state_t *s, enum calculator_mode mode) {
s->mode = mode;
s->fn_index = 0;
show_fn(s, 0);
// Faster tick in operation mode so we can blink.
movement_request_tick_frequency(mode == CALC_OPERATION ? 4 : 1);
}
// Binary-ish search to find the right number. direction is +1 if it should be bigger, -1 if it should be smaller.
static void adjust_number(calculator_state_t *s, int direction) {
if (direction > 0) {
s->min = C;
} else {
s->max = C;
}
// If the direction we want to go has no bound (i.e. isnan),
// then first get the sign right (moving to 0, then +-10), and
// after than go up by *10.
if (isnan(direction > 0 ? s->max : s->min)) {
if (direction * C < 0) {
C = 0;
} else if (C == 0) {
C = direction * 10;
} else {
C *= 10;
}
} else {
// We have a higher and lower bound. Split them.
C = (s->max + s->min) / 2;
// Subtract 0.1 so we don't apply most significant rounding to things that are _exactly_ 1/10/100 apart.
double mag = log10(fabs(s->max - s->min)) - 0.1;
if (mag > 0.0) {
// i.e. the different is >= 2, which means we want to round aggressively
// to not show people complicated looking numbers.
// e.g. this takes a number like 3.2 to 3, or a number like 464 to 500
// (depending on how fine-grained 'mag' tells us to be).
float div = pow(10, floor(mag));
int sign = C < 0 ? -1 : 1;
C = sign * floor(fabs(C) / div) * div;
}
}
}
static void fn_number(calculator_state_t *s) {
PUSH(10);
s->min = s->max = NAN;
change_mode(s, CALC_NUMBER);
}
static void fn_add(calculator_state_t *s) {
double a = POP();
double b = POP();
PUSH(a + b);
}
static void fn_sub(calculator_state_t *s) {
double a = POP();
double b = POP();
PUSH(b - a);
}
static void fn_mul(calculator_state_t *s) {
double a = POP();
double b = POP();
PUSH(a * b);
}
static void fn_div(calculator_state_t *s) {
double a = POP();
double b = POP();
PUSH(b / a);
}
static void fn_pow(calculator_state_t *s) {
double a = POP();
double b = POP();
PUSH(pow(b, a));
}
static void fn_sqrt(calculator_state_t *s) {
double x = POP();
PUSH(sqrt(x));
}
static void fn_log(calculator_state_t *s) {
double x = POP();
PUSH(log(x));
}
static void fn_log10(calculator_state_t *s) {
double x = POP();
PUSH(log10(x));
}
static void fn_e(calculator_state_t *s) {
PUSH(M_E);
}
static void fn_sin(calculator_state_t *s) {
double x = POP();
PUSH(sin(x));
}
static void fn_cos(calculator_state_t *s) {
double x = POP();
PUSH(cos(x));
}
static void fn_tan(calculator_state_t *s) {
double x = POP();
PUSH(tan(x));
}
static void fn_pi(calculator_state_t *s) {
PUSH(M_PI);
}
static void fn_pop(calculator_state_t *s) {
--s->stack_size;
}
static void fn_swap(calculator_state_t *s) {
double a = POP();
double b = POP();
PUSH(a);
PUSH(b);
}
static void fn_duplicate(calculator_state_t *s) {
double a = POP();
PUSH(a);
PUSH(a);
}
static void fn_clear(calculator_state_t *s) {
s->stack_size = 0;
}
static void fn_size(calculator_state_t *s) {
double a = s->stack_size;
PUSH(a);
}
struct {
char name[2];
uint8_t input;
uint8_t output;
void (*func)(calculator_state_t *);
} functions[] = {
{{'n', 'o'}, 0, 1, fn_number},
{{'*', ' '}, 2, 1, fn_add}, // First position * actually looks like a '+'.
{{'-', ' '}, 2, 1, fn_sub},
{{'H', ' '}, 2, 1, fn_mul}, // For actual *, we throw in the middle vertical segment onto the H.
{{'/', ' '}, 2, 1, fn_div}, // There's also some minor hackery on '/'.
{{'P', 'o'}, 2, 1, fn_pow},
{{'S', 'r'}, 1, 1, fn_sqrt},
{{'L', 'n'}, 1, 1, fn_log},
{{'L', 'o'}, 1, 1, fn_log10},
{{'e', ' '}, 0, 1, fn_e},
{{'P', 'i'}, 0, 1, fn_pi},
{{'C', 'o'}, 1, 1, fn_cos},
{{'S', 'i'}, 1, 1, fn_sin},
{{'T', 'a'}, 1, 1, fn_tan},
// Stack operations. Accessible via secondary_fn_index (i.e. alarm long press).
{{'P', 'O'}, 1, 0, fn_pop}, // This ends up displaying the same as 'POW'. But at least it's in a different place.
{{'S', 'W'}, 2, 2, fn_swap},
{{'d', 'u'}, 1, 1, fn_duplicate}, // Uppercase 'D' is a bit too 'O' for me.
{{'C', 'L'}, 1, 0, fn_clear}, // Operation lie - takes _everything_ off the stack, but a check of 1 is sufficient.
{{'L', 'E'}, 1, 0, fn_size},
};
#define FUNCTIONS_LEN (sizeof(functions) / sizeof(functions[0]))
#define SECONDARY_FN_INDEX (FUNCTIONS_LEN - 4)
// Show the function name (using day display)
static void show_fn(calculator_state_t *s, uint8_t subsecond) {
if (subsecond % 2) {
// blink
watch_display_string(" ", 0);
return;
}
char *name = functions[s->fn_index].name;
char buf[3] = {name[0], name[1], '\0'};
watch_display_string(buf, 0);
// The first position has a bunch of segments, and I have minor
// disagreements with the character set choices in watch_display_string,
// so we tweak a little here.
switch (buf[0]) {
case 'H':
// Use the middle segment lines to make our 'H' a '*'-ish thing.
watch_set_pixel(1, 14);
break;
case '/':
// Add a middle bar to division.
watch_set_pixel(1, 15);
break;
default:
break;
}
}
// Show the top of the stack (using everything except day display).
static void show_stack_top(calculator_state_t *s) {
if (s->stack_size > 0) {
show_number(C);
} else {
watch_display_string(" ------", 2);
watch_clear_colon();
}
}
bool rpn_calculator_alt_face_loop(movement_event_t event, void *context) {
calculator_state_t *s = (calculator_state_t *)context;
int proposed_stack_size;
switch (event.event_type) {
case EVENT_ACTIVATE:
change_mode(s, CALC_OPERATION);
show_stack_top(s);
break;
case EVENT_TICK:
if (s->mode == CALC_OPERATION) {
show_fn(s, event.subsecond);
}
break;
case EVENT_MODE_BUTTON_UP:
if (s->mode == CALC_NUMBER) {
adjust_number(s, -1);
show_stack_top(s);
} else {
movement_move_to_next_face();
return false;
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
proposed_stack_size = s->stack_size - functions[s->fn_index].input;
if (s->mode == CALC_NUMBER) {
change_mode(s, CALC_OPERATION);
} else if (proposed_stack_size < 0 || proposed_stack_size + functions[s->fn_index].output > CALC_MAX_STACK_SIZE) {
movement_play_signal();
break;
} else {
functions[s->fn_index].func(s);
show_stack_top(s);
s->fn_index = 0;
show_fn(s, 0);
}
break;
case EVENT_ALARM_BUTTON_UP:
if (s->mode == CALC_NUMBER) {
adjust_number(s, 1);
show_stack_top(s);
} else {
s->fn_index = (s->fn_index + 1) % FUNCTIONS_LEN;
show_fn(s, 0);
}
break;
case EVENT_ALARM_LONG_PRESS:
if (s->mode == CALC_OPERATION) {
if (s->fn_index == 0) {
s->fn_index = SECONDARY_FN_INDEX;
} else {
s->fn_index = 0;
}
show_fn(s, 0);
}
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
case EVENT_LOW_ENERGY_UPDATE:
break;
default:
movement_default_loop_handler(event);
break;
}
// return true if the watch can enter standby mode. If you are PWM'ing an LED or buzzing the buzzer here,
// you should return false since the PWM driver does not operate in standby mode.
return true;
}
void rpn_calculator_alt_face_resign(void *context) {
(void) context;
// handle any cleanup before your watch face goes off-screen.
}

View File

@@ -0,0 +1,96 @@
/*
* MIT License
*
* Copyright (c) 2022 <#author_name#>
*
* 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 CALCULATOR_FACE_H_
#define CALCULATOR_FACE_H_
/*
* RPN Calculator alternate face.
*
* Operations appear in the 'day' section; ALARM changes between operations when
* operation is flashing. LIGHT executes current operation.
*
* This is the alternate face because it has a non-traditional number entry system which
* I call 'guess a number'. In number entry mode, the watch tries to guess which number you
* want, and you respond with 'smaller' (left - MODE) or larger (right - ALARM). This means
* that when you _are_ entering a number, MODE will no longer move between faces!
*
* Example of entering the number 27
* - select the NO operation (probably unnecessary, as this is the default),
* and execute it by hitting LIGHT.
* - you are now in number entry mode; you know this because nothing is flashing.
* - Watch displays 10; you hit ALARM to say you want a larger number.
* - Watch displays 100; you hit MODE to say you want a smaller number.
* - Continuing: 50 -> MODE -> 30 -> MODE -> 20 -> ALARM -> 27
* - Hit LIGHT to add the number to the stack (and now 'NO' is flashing
* again, indicating you're back in operation selection mode).
*
* One other thing to watch out for is how quickly it will switch into scientific notation
* due to the limitations of the display when you have large numbers or non-integer values.
* In this mode, the 'colon' serves at the decimal point, and the numbers in the top right
* are the exponent.
*
* As with the main movement firmware, this has the concept of 'secondary' functions which
* you can jump to by a long hold of ALARM on NO. These are functions to do with stack
* manipulation (pop, swap, dupe, clear, size (le)). If you're _not_ on NO, a long
* hold will take you back to it.
*
* See 'functions' in "rpn_calculator_alt_face.c" for names of all operations.
*/
#include "movement.h"
#define CALC_MAX_STACK_SIZE 20
enum calculator_mode {
CALC_OPERATION = 0,
CALC_NUMBER,
};
typedef struct {
double stack[CALC_MAX_STACK_SIZE];
uint8_t stack_size; // this is the current stack top + 1 (so that '0' means nothing on the stack)
uint8_t fn_index;
double min;
double max;
enum calculator_mode mode;
} calculator_state_t;
void rpn_calculator_alt_face_setup(uint8_t watch_face_index, void ** context_ptr);
void rpn_calculator_alt_face_activate(void *context);
bool rpn_calculator_alt_face_loop(movement_event_t event, void *context);
void rpn_calculator_alt_face_resign(void *context);
#define rpn_calculator_alt_face ((const watch_face_t){ \
rpn_calculator_alt_face_setup, \
rpn_calculator_alt_face_activate, \
rpn_calculator_alt_face_loop, \
rpn_calculator_alt_face_resign, \
NULL, \
})
#endif // CALCULATOR_FACE_H_

View File

@@ -0,0 +1,346 @@
/*
* MIT License
*
* Copyright (c) 2022 Niclas Hoyer
*
* 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 <math.h>
#include "rpn_calculator_face.h"
static void draw_number(char *buf, float num) {
float f = fmodf(num, 1.0) * 100;
sprintf(buf, "CA %4d%02d", ((int)num) % 10000, (int) f);
}
static void draw_op(char *buf, rpn_calculator_op_t op) {
switch (op) {
case rpn_calculator_op_add:
sprintf(buf, "CA Add");
break;
case rpn_calculator_op_sub:
sprintf(buf, "CA sub");
break;
case rpn_calculator_op_mul:
sprintf(buf, "CA n&ul");
break;
case rpn_calculator_op_div:
sprintf(buf, "CA div");
break;
case rpn_calculator_op_pow:
sprintf(buf, "CA pow");
break;
case rpn_calculator_op_sqrt:
sprintf(buf, "CA sqrt");
break;
case rpn_calculator_op_pi:
sprintf(buf, "CA pi");
break;
default:
break;
}
}
static void printf_stack(rpn_calculator_state_t *state) {
printf("Stack: [%f, %f, %f, %f], top: %d\n",
state->stack[0],
state->stack[1],
state->stack[2],
state->stack[3],
state->top
);
}
static void next_op(rpn_calculator_state_t *state) {
state->op += 1;
state->op = state->op % RPN_CALCULATOR_MAX_OPS;
}
// increase a digit of a floating point number
// FIXME: this converts the number to string and back, there might
// be better ways to do this
static float inc_digit(float num, uint8_t position) {
char buf[8];
if (position > 5) {
return 0.0;
}
// inverse position (rtl)
position = 5 - position;
float f = fmodf(num, 1.0) * 100;
sprintf(buf, "%04d%02d", ((int)num) % 10000, (int) f);
uint8_t digit = buf[position] - '0';
digit = (digit + 1) % 10;
buf[position] = digit + '0';
printf("buf: %s\n", buf);
return strtof(buf, NULL) / 100.0;
}
static void stack_push(rpn_calculator_state_t *state, float f) {
printf_stack(state);
printf("push: %f\n", f);
state->top++;
if (state->top >= RPN_CALCULATOR_STACK_SIZE) {
// FIXME: implement this using a circular buffer?
for (int i=0; i<RPN_CALCULATOR_STACK_SIZE-1; i++) {
state->stack[i] = state->stack[i+1];
}
}
state->stack[state->top] = f;
}
static float stack_peek(rpn_calculator_state_t *state) {
if (state->top > -1) {
return state->stack[state->top];
}
return 0;
}
static float stack_pop(rpn_calculator_state_t *state) {
printf_stack(state);
float f = stack_peek(state);
state->stack[state->top] = 0;
printf("pop: %f\n", f);
if (state->top > -1) {
state->top--;
} else {
state->top = -1; // empty
}
return f;
}
static void run_op(rpn_calculator_state_t *state) {
printf_stack(state);
bool op_found = false;
// ops without parameters
switch (state->op) {
case rpn_calculator_op_pi:
stack_push(state, M_PI);
op_found = true;
break;
default:
break;
}
if (op_found) {
state->mode = rpn_calculator_waiting;
return;
}
// ops with one parameter
if (state->top < 0) {
state->mode = rpn_calculator_err;
return;
}
float right = stack_pop(state);
printf("right: %f\n", right);
switch (state->op) {
case rpn_calculator_op_sqrt:
stack_push(state, sqrt(right));
op_found = true;
break;
default:
break;
}
if (op_found) {
state->mode = rpn_calculator_waiting;
return;
}
// ops with two parameters
if (state->top < 0) {
// no parameter left -> error
state->mode = rpn_calculator_err;
return;
}
float left = stack_pop(state);
printf("left: %f\n", left);
switch (state->op) {
case rpn_calculator_op_add:
stack_push(state, left + right);
op_found = true;
break;
case rpn_calculator_op_sub:
stack_push(state, left - right);
op_found = true;
break;
case rpn_calculator_op_mul:
stack_push(state, left * right);
op_found = true;
break;
case rpn_calculator_op_div:
stack_push(state, left / right);
op_found = true;
break;
case rpn_calculator_op_pow:
stack_push(state, powf(left, right));
op_found = true;
break;
default:
break;
}
if (op_found) {
state->mode = rpn_calculator_waiting;
return;
}
state->mode = rpn_calculator_err;
}
static void draw(rpn_calculator_state_t *state, uint8_t subsecond) {
char buf[16];
switch (state->mode) {
case rpn_calculator_err:
sprintf(buf, "CA err ");
break;
case rpn_calculator_number:
draw_number(buf, stack_peek(state));
uint8_t i = 4 + (5 - state->selection);
if (buf[i] == ' ') {
buf[i] = '0';
}
if (subsecond % 2) {
buf[i] = ' ';
}
break;
case rpn_calculator_waiting:
printf_stack(state);
draw_number(buf, stack_peek(state));
break;
case rpn_calculator_op:
draw_op(buf, state->op);
break;
default:
break;
}
watch_display_string(buf, 0);
}
void rpn_calculator_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(rpn_calculator_state_t));
memset(*context_ptr, 0, sizeof(rpn_calculator_state_t));
// Do any one-time tasks in here; the inside of this conditional happens only at boot.
rpn_calculator_state_t *state = *context_ptr;
state->top = -1;
}
// Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.
}
void rpn_calculator_face_activate(void *context) {
(void) context;
// Handle any tasks related to your watch face coming on screen.
}
bool rpn_calculator_face_loop(movement_event_t event, void *context) {
rpn_calculator_state_t *state = (rpn_calculator_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
draw(state, event.subsecond);
break;
case EVENT_TICK:
if (state->mode == rpn_calculator_number) {
draw(state, event.subsecond);
}
break;
case EVENT_MODE_BUTTON_UP:
switch (state->mode) {
case rpn_calculator_number:
state->mode = rpn_calculator_waiting;
draw(state, event.subsecond);
movement_request_tick_frequency(1);
break;
default:
state->mode = rpn_calculator_waiting;
movement_request_tick_frequency(1);
movement_move_to_next_face();
break;
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
switch (state->mode) {
case rpn_calculator_waiting:
state->mode = rpn_calculator_op;
draw(state, event.subsecond);
break;
case rpn_calculator_number:
state->selection = (state->selection + 1) % 6;
draw(state, event.subsecond);
break;
case rpn_calculator_op:
next_op(state);
draw(state, event.subsecond);
break;
default:
movement_illuminate_led();
break;
}
break;
case EVENT_ALARM_BUTTON_UP:
switch (state->mode) {
case rpn_calculator_waiting:
state->mode = rpn_calculator_number;
state->selection = 2;
stack_push(state, 0);
draw(state, event.subsecond);
movement_request_tick_frequency(4);
break;
case rpn_calculator_number:
state->stack[state->top] = inc_digit(state->stack[state->top], state->selection);
printf_stack(state);
draw(state, event.subsecond);
break;
case rpn_calculator_err:
state->mode = rpn_calculator_waiting;
draw(state, event.subsecond);
break;
case rpn_calculator_op:
run_op(state);
draw(state, event.subsecond);
break;
default:
break;
}
break;
case EVENT_TIMEOUT:
state->mode = rpn_calculator_waiting;
movement_request_tick_frequency(1);
movement_move_to_face(0);
break;
case EVENT_LOW_ENERGY_UPDATE:
break;
default:
movement_default_loop_handler(event);
break;
}
// return true if the watch can enter standby mode. If you are PWM'ing an LED or buzzing the buzzer here,
// you should return false since the PWM driver does not operate in standby mode.
return true;
}
void rpn_calculator_face_resign(void *context) {
(void) context;
// handle any cleanup before your watch face goes off-screen.
}

View File

@@ -0,0 +1,81 @@
/*
* MIT License
*
* Copyright (c) 2022 Niclas Hoyer
*
* 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 RPN_CALCULATOR_FACE_H_
#define RPN_CALCULATOR_FACE_H_
/*
* RPN CALCULATOR face
*
* A calculator face using reverse polish notation (RPN).
*
* For usage instructions, please refer to the wiki:
* https://www.sensorwatch.net/docs/watchfaces/complication/#rpn-calculator
*/
#include "movement.h"
#define RPN_CALCULATOR_STACK_SIZE 4
#define RPN_CALCULATOR_MAX_OPS 7;
typedef enum {
rpn_calculator_op_add,
rpn_calculator_op_sub,
rpn_calculator_op_mul,
rpn_calculator_op_div,
rpn_calculator_op_pow,
rpn_calculator_op_sqrt,
rpn_calculator_op_pi
} rpn_calculator_op_t;
typedef enum {
rpn_calculator_waiting,
rpn_calculator_op,
rpn_calculator_number,
rpn_calculator_err,
} rpn_calculator_mode_t;
typedef struct {
rpn_calculator_mode_t mode;
rpn_calculator_op_t op;
float stack[RPN_CALCULATOR_STACK_SIZE];
int8_t top;
uint8_t selection;
} rpn_calculator_state_t;
void rpn_calculator_face_setup(uint8_t watch_face_index, void ** context_ptr);
void rpn_calculator_face_activate(void *context);
bool rpn_calculator_face_loop(movement_event_t event, void *context);
void rpn_calculator_face_resign(void *context);
#define rpn_calculator_face ((const watch_face_t){ \
rpn_calculator_face_setup, \
rpn_calculator_face_activate, \
rpn_calculator_face_loop, \
rpn_calculator_face_resign, \
NULL, \
})
#endif // RPN_CALCULATOR_FACE_H_

View File

@@ -0,0 +1,409 @@
/*
* MIT License
*
* Copyright (c) 2023 Jan H. Voigt
* Copyright (c) 2022 Wesley Ellis
* Copyright (c) 2022 Niclas Hoyer
*
* 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 "sailing_face.h"
#include "watch.h"
#include "watch_utility.h"
#define sl_SELECTIONS 6
#define DEFAULT_MINUTES { 5,4,1,0,0,0 }
static inline int32_t get_tz_offset() {
return movement_get_current_timezone_offset();
}
static int lap = 0;
bool ringflag = false;
int8_t double_beep[] = {BUZZER_NOTE_C8, 4, BUZZER_NOTE_REST, 5, BUZZER_NOTE_C8, 5, 0};
int8_t single_beep[] = {BUZZER_NOTE_C8, 4, 0};
int8_t long_beep[] = {BUZZER_NOTE_C8, 40, 0};
int beepseconds[] = {600, 540, 480, 420, 360, 300, 240, 180, 120, 60, 30, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; //seconds before start that can trigger the buzzer. Every whole minute, 30s before start & 10s countdown.
int beepseconds_size = sizeof(beepseconds) / sizeof(int);
int beepflag = 0;
int alarmflag = 3;
static void reset(sailing_state_t *state) {
state->index = 0;
state->mode = sl_waiting;
movement_cancel_background_task();
watch_clear_indicator(WATCH_INDICATOR_LAP);
beepflag = 0;
ringflag = false;
}
static void counting(sailing_state_t *state) {
state->mode = sl_counting;
movement_cancel_background_task();
watch_set_indicator(WATCH_INDICATOR_LAP);
}
static void draw(sailing_state_t *state, uint8_t subsecond) {
char tmp[24];
char buf[16];
uint32_t delta;
div_t result;
uint8_t hrs, min, sec;
switch (state->mode) {
case sl_running:
if (state->now_ts > state->target_ts) {
delta = 0;
counting(state); //in case buttons are pressed while sound is played (esp. in the last 10s), the timing of ring might be thrown off. Beep stops and the switch to counting doesn't occur. temporary fix/safety net.
} else {
delta = state->target_ts - state->now_ts;
}
result = div(delta, 60);
min = result.quot;
sec = result.rem;
if (min > 0) {
sprintf(buf, "SA1L %2d%02d", min, sec);
} else {
sprintf(buf, "SA1L %2d ", sec);
}
break;
case sl_waiting:
sprintf(buf, "SA1L %2d%02d", state->minutes[0], 0);
break;
case sl_setting:
// this sprintf to a larger tmp is to guarantee that no buffer overflows
// occur here (and to squelch the corresponding compiler warning)
if (state->minutes[0] == 10) { //print 10 vertically.
sprintf(tmp, "SA1L %1d%1d%1d%1d%1d",
state->minutes[1],
state->minutes[2],
state->minutes[3],
state->minutes[4],
state->minutes[5]
);
memcpy(buf, tmp, sizeof(buf));
if (subsecond % 2) {
buf[4 + state->selection] = ' ';
}
watch_display_string(buf, 0);
if (!(subsecond % 2) || state->selection != 0) {
watch_set_pixel(0, 18);
watch_set_pixel(0, 19);
watch_set_pixel(1, 18);
watch_set_pixel(1, 19);
}
return;
}
sprintf(tmp, "SA1L%1d%1d%1d%1d%1d%1d",
state->minutes[0],
state->minutes[1],
state->minutes[2],
state->minutes[3],
state->minutes[4],
state->minutes[5]
);
memcpy(buf, tmp, sizeof(buf));
if (subsecond % 2) {
buf[4 + state->selection] = ' ';
}
break;
case sl_counting:
delta = state->now_ts - state->target_ts;
if (state->now_ts <= state->target_ts) {
sprintf(buf, "SA1L %2d ", 0);
}
else {
result = div(delta, 3600);
hrs = result.quot;
delta -= 60*hrs;
result = div(delta, 60);
min = result.quot;
sec = result.rem;
sprintf(buf, "SL%2d%2d%02d%02d", lap, hrs, min, sec);//implement counting
if (hrs > 23) {
reset(state);
}
}
break;
}
watch_display_string(buf, 0);
}
static void ring(sailing_state_t *state) {
// if ring is called in background (while on another face), a button press can interrupt and cancel the execution.
// To reduce the probability of cancelling all future alarms, the new alarm is set as soon as possible after calling ring.
movement_cancel_background_task();
if (beepflag + 1 == beepseconds_size) { //equivalent to (beepflag + 1 == sizeof(beepseconds) / sizeof(int)) but without needing to divide here => quicker
if (alarmflag != 0){
watch_buzzer_play_sequence(long_beep, NULL);
}
movement_cancel_background_task();
counting(state);
return;
}
state->nextbeep_ts = state->target_ts - beepseconds[beepflag+1];
watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(state->nextbeep_ts, get_tz_offset(settings));
movement_schedule_background_task_for_face(state->watch_face_index, target_dt);
//background task is set, now we have time to play the tune. If this is cancelled accidentally, the next alarm will still ring. Sound is implemented non-blocking, so that neither buttons nor display output are compromised.
for (int i = 0; i < 5; i++) {
if (beepseconds[beepflag] == 60 * state->minutes[i]) {
if (alarmflag > 1) {
watch_buzzer_play_sequence((int8_t *)double_beep, NULL);
}
ringflag = true;
}
}
if (!ringflag) {
if (alarmflag == 3) {
watch_buzzer_play_sequence((int8_t *)single_beep, NULL);
}
}
ringflag = false;
beepflag++;
}
static void start(sailing_state_t *state) {//gets called by starting / switching to next signal
while (beepseconds[beepflag] < state->minutes[state->index]*60) {
state->index++;
}
while (beepseconds[beepflag] > state->minutes[state->index]*60) {
beepflag++;
}
if (state->index > 5 || state->minutes[state->index] == 0) {
watch_date_time_t now = watch_rtc_get_date_time();
state->now_ts = watch_utility_date_time_to_unix_time(now, get_tz_offset(settings));
state->target_ts = state->now_ts;
if (alarmflag != 0){
watch_buzzer_play_sequence(long_beep, NULL);
}
counting(state);
return;
}
movement_request_tick_frequency(1); //synchronises tick with the moment the button was pressed. Solves 1s offset between sound and display, solves up to +-0.5s offset between button action and display.
state->mode = sl_running;
watch_date_time_t now = watch_rtc_get_date_time();
state->now_ts = watch_utility_date_time_to_unix_time(now, get_tz_offset(settings));
state->target_ts = watch_utility_offset_timestamp(state->now_ts, 0, state->minutes[state->index], 0);
ring(state, settings);
}
static void settings_increment(sailing_state_t *state) {
state->minutes[state->selection] += 1;
uint8_t max = 11;
if (state->selection > 0) {
max = state->minutes[state->selection-1];
}
if (state->minutes[state->selection] >= max) {
state->minutes[state->selection] = 0;
}
// ensure that minutes are decreasing
if (state->selection < 5) {
for (uint8_t i = 0; i < 5; i++) {
if (state->minutes[i+1] >= state->minutes[i]) {
if (state->minutes[i] > 0) {
state->minutes[i+1] = state->minutes[i] - 1;
} else {
state->minutes[i+1] = 0;
}
}
}
}
return;
}
void sailing_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(sailing_state_t));
sailing_state_t *state = (sailing_state_t *)*context_ptr;
memset(*context_ptr, 0, sizeof(sailing_state_t));
static const uint8_t default_minutes[6] = DEFAULT_MINUTES;
memcpy(&state->minutes, default_minutes, sizeof(default_minutes));
state->watch_face_index = watch_face_index;
}
}
void sailing_face_activate(void *context) {
sailing_state_t *state = (sailing_state_t *)context;
if(state->mode == sl_running) {
watch_date_time_t now = watch_rtc_get_date_time();
state->now_ts = watch_utility_date_time_to_unix_time(now, get_tz_offset(settings));
}
if(state->mode == sl_counting) {
watch_date_time_t now = watch_rtc_get_date_time();
state->now_ts = watch_utility_date_time_to_unix_time(now, get_tz_offset(settings));
watch_set_indicator(WATCH_INDICATOR_LAP);
}
switch (alarmflag) {
case 0: //no sound
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
break;
case 1: //sound at start only
watch_set_indicator(WATCH_INDICATOR_BELL);
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
break;
case 2: //sound at set minutes
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
break;
case 3: //sound at every minute, 30s, 10-0s
watch_set_indicator(WATCH_INDICATOR_BELL);
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
break;
}
}
bool sailing_face_loop(movement_event_t event, void *context) {
sailing_state_t *state = (sailing_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
draw(state, event.subsecond, settings);
break;
case EVENT_TICK:
if (state->mode == sl_running || state->mode == sl_counting) {
state->now_ts++;
}
draw(state, event.subsecond, settings);
break;
case EVENT_LIGHT_LONG_PRESS:
if (state->mode == sl_running) {
reset(state);
}
if (state->mode == sl_counting) {
reset(state);
}
if (state->mode == sl_setting) {
if (alarmflag == 3) {
alarmflag = 0;
}
else {
alarmflag++;
}
switch (alarmflag) {
case 0: //no sound
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
break;
case 1: //sound at start only
watch_set_indicator(WATCH_INDICATOR_BELL);
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
break;
case 2: //sound at set minutes
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
break;
case 3: //sound at every minute, 30s, 10-0s
watch_set_indicator(WATCH_INDICATOR_BELL);
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
break;
}
}
break;
case EVENT_LIGHT_BUTTON_UP:
switch(state->mode) {
case sl_running:
movement_illuminate_led();
break;
case sl_waiting:
state->mode = sl_setting;
movement_request_tick_frequency(4);
break;
case sl_setting:
state->selection++;
if(state->selection >= sl_SELECTIONS) {
state->selection = 0;
state->mode = sl_waiting;
movement_request_tick_frequency(1);
}
break;
case sl_counting:
movement_illuminate_led();
break;
}
draw(state, event.subsecond, settings);
break;
case EVENT_ALARM_BUTTON_UP:
switch(state->mode) {
case sl_running:
start(state, settings);
break;
case sl_waiting:
start(state, settings);
break;
case sl_setting:
settings_increment(state);
break;
case sl_counting:
//implement lap counting up to 39
if (lap <39){
lap++;
}
break;
}
draw(state, event.subsecond, settings);
break;
case EVENT_BACKGROUND_TASK:
ring(state, settings);
break;
case EVENT_ALARM_LONG_PRESS:
if (state->mode == sl_setting) {
static const uint8_t default_minutes[6] = DEFAULT_MINUTES;
memcpy(&state->minutes, default_minutes, sizeof(default_minutes));
state->index = 0;
draw(state, event.subsecond, settings);
break;
}
if (state->mode == sl_counting) {
lap = 0;
}
break;
case EVENT_TIMEOUT:
if (state->mode != sl_running && state->mode != sl_counting) {
movement_move_to_face(0);
}
break;
case EVENT_LOW_ENERGY_UPDATE:
break;
case EVENT_LIGHT_BUTTON_DOWN:
// don't light up every time light is hit
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void sailing_face_resign(void *context) {
sailing_state_t *state = (sailing_state_t *)context;
if (state->mode == sl_setting) {
state->selection = 0;
state->mode = sl_waiting;
}
}

View File

@@ -0,0 +1,97 @@
/*
* MIT License
*
* Copyright (c) 2023 Jan H. Voigt
* Copyright (c) 2022 Wesley Ellis
* Copyright (c) 2022 Niclas Hoyer
*
* 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 SAILING_FACE_H_
#define SAILING_FACE_H_
/*
* SAILING face
* Implements a sailing timer.
*
* Usage:
*
* Waiting mode:
* LIGHT button enters settings
* ALARM button starts the timer (sailing mode).
*
* Sailing mode:
* ALARM button switches to next programmed start signal.
* Long press on LIGHT button resets timer and enters waiting mode.
* Countdown to zero, then switch to counting mode.
*
* Counting mode:
* After the start signal (0s), the duration of the race is counted (like a stopwatch timer).
* ALARM button increases the lap counter, ALARM long press resets lap counter.
* Long press on LIGHT button resets timer and enters waiting mode.
*
* Setting mode:
* ALARM button increases active (blinking) signal. Goes to 0 if upper boundary
* (11 or whatever the signal left to the active one is set to) is met.
* 10 is printed vertically (letter o plus top segment).
* ALARM button long press resets to default minutes (5-4-1-0).
* LIGHT button cycles through the signals.
* Long press on LIGHT button cycles through sound modes:
* - Bell indicator: Sound at start (0s) only.
* - Signal indicator: Sound at each programmed signal and at start.
* - Bell+Signal: Sound at each minute, at 30s and at 10s countdown.
* - No indicator: No sound.
*/
#include "movement.h"
typedef enum {
sl_waiting,
sl_running,
sl_setting,
sl_counting
} sailing_mode_t;
typedef struct {
uint8_t watch_face_index;
uint32_t target_ts;
uint32_t now_ts;
uint32_t nextbeep_ts;
uint8_t index;
uint8_t minutes[6];
uint8_t selection;
sailing_mode_t mode;
} sailing_state_t;
void sailing_face_setup(uint8_t watch_face_index, void ** context_ptr);
void sailing_face_activate(void *context);
bool sailing_face_loop(movement_event_t event, void *context);
void sailing_face_resign(void *context);
#define sailing_face ((const watch_face_t){ \
sailing_face_setup, \
sailing_face_activate, \
sailing_face_loop, \
sailing_face_resign, \
NULL, \
})
#endif // sailing_FACE_H_

View File

@@ -0,0 +1,159 @@
/*
* MIT License
*
* Copyright (c) 2023 buckket
*
* 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 <stdio.h>
#include <string.h>
#include "ships_bell_face.h"
static void ships_bell_ring() {
watch_date_time_t date_time = watch_rtc_get_date_time();
date_time.unit.hour %= 4;
date_time.unit.hour = date_time.unit.hour == 0 && date_time.unit.minute < 30 ? 4 : date_time.unit.hour;
for (uint8_t i = 0; i < date_time.unit.hour; i++) {
watch_buzzer_play_note(BUZZER_NOTE_C8, 75);
watch_buzzer_play_note(BUZZER_NOTE_REST, 75);
watch_buzzer_play_note(BUZZER_NOTE_C8, 100);
watch_buzzer_play_note(BUZZER_NOTE_REST, 250);
}
if (date_time.unit.minute >= 30 ? 1 : 0) {
watch_buzzer_play_note(BUZZER_NOTE_C8, 100);
}
}
static void ships_bell_draw(ships_bell_state_t *state) {
char buf[8];
if (state->on_watch) {
sprintf(buf, "%d", state->on_watch);
} else {
sprintf(buf, " ");
}
watch_date_time_t date_time = watch_rtc_get_date_time();
date_time.unit.hour %= 4;
sprintf(buf + 1, " %d%02d%02d", date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
watch_display_string(buf, 3);
}
void ships_bell_face_setup(uint8_t watch_face_index, void **context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(ships_bell_state_t));
memset(*context_ptr, 0, sizeof(ships_bell_state_t));
}
}
void ships_bell_face_activate(void *context) {
ships_bell_state_t *state = (ships_bell_state_t *) context;
if (state->bell_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
else watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_display_string("SB", 0);
watch_set_colon();
}
bool ships_bell_face_loop(movement_event_t event, void *context) {
ships_bell_state_t *state = (ships_bell_state_t *) context;
switch (event.event_type) {
case EVENT_ACTIVATE:
case EVENT_TICK:
ships_bell_draw(state);
break;
case EVENT_ALARM_BUTTON_UP:
state->bell_enabled = !state->bell_enabled;
if (state->bell_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
else watch_clear_indicator(WATCH_INDICATOR_BELL);
break;
case EVENT_ALARM_LONG_PRESS:
state->on_watch = (state->on_watch + 1) % 4;
ships_bell_draw(state);
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
case EVENT_LOW_ENERGY_UPDATE:
break;
case EVENT_BACKGROUND_TASK:
if (watch_is_buzzer_or_led_enabled()) {
ships_bell_ring();
} else {
watch_enable_buzzer();
ships_bell_ring();
watch_disable_buzzer();
}
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void ships_bell_face_resign(void *context) {
(void) context;
}
movement_watch_face_advisory_t ships_bell_face_advise(void *context) {
ships_bell_state_t *state = (ships_bell_state_t *) context;
movement_watch_face_advisory_t retval = { 0 };
if (!state->bell_enabled) return retval;
watch_date_time_t date_time = watch_rtc_get_date_time();
if (!(date_time.unit.minute == 0 || date_time.unit.minute == 30)) return retval;
date_time.unit.hour %= 12;
// #SecondMovement: This was migrated to the new advisory API but not tested. Needs more testing!
switch (state->on_watch) {
case 1:
if ((date_time.unit.hour >= 4 && date_time.unit.hour < 8) ||
(date_time.unit.hour == 8 && date_time.unit.minute == 0))
retval.wants_background_task = true;
break;
case 2:
if ((date_time.unit.hour >= 8 && date_time.unit.hour < 12) ||
(date_time.unit.hour == 0 && date_time.unit.minute == 0))
retval.wants_background_task = true;
break;
case 3:
if ((date_time.unit.hour >= 0 && date_time.unit.hour < 4) ||
(date_time.unit.hour == 4 && date_time.unit.minute == 0))
retval.wants_background_task = true;
break;
default:
retval.wants_background_task = true;
}
return retval;
}

View File

@@ -0,0 +1,68 @@
/*
* MIT License
*
* Copyright (c) 2023 buckket
*
* 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 SHIPS_BELL_FACE_H_
#define SHIPS_BELL_FACE_H_
/*
* SHIP'S BELL face
* A ship's bell complication.
*
* See: https://en.wikipedia.org/wiki/Ship%27s_bell#Simpler_system
*
* Similar to the default hourly signal of the simple_clock_face this complication will use the buzzer to signal
* the time in half-hour intervals according to the scheme mentioned above.
*
* Additionally, the user can specify one of the three watches
* of the standard merchant watch system to only receive signals during this watch.
*
* If no watch is specified all signals are emitted.
*
* Usage:
* - short press Alarm button: Turn on/off bell
* - long press Alarm button: Cycle through the watches (All/1/2/3)
*/
#include "movement.h"
typedef struct {
bool bell_enabled;
uint8_t on_watch;
} ships_bell_state_t;
void ships_bell_face_setup(uint8_t watch_face_index, void ** context_ptr);
void ships_bell_face_activate(void *context);
bool ships_bell_face_loop(movement_event_t event, void *context);
void ships_bell_face_resign(void *context);
movement_watch_face_advisory_t ships_bell_face_advise(void *context);
#define ships_bell_face ((const watch_face_t){ \
ships_bell_face_setup, \
ships_bell_face_activate, \
ships_bell_face_loop, \
ships_bell_face_resign, \
ships_bell_face_advise, \
})
#endif // SHIPS_BELL_FACE_H_

View File

@@ -0,0 +1,333 @@
/*
* MIT License
*
* Copyright (c) 2024 <#author_name#>
*
* 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 "simon_face.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Emulator only: need time() to seed the random number generator
#if __EMSCRIPTEN__
#include <time.h>
#endif
static char _simon_display_buf[12];
static uint8_t _timer;
static uint16_t _delay_beep;
static uint16_t _timeout;
static uint8_t _secSub;
static inline uint8_t _simon_get_rand_num(uint8_t num_values) {
#if __EMSCRIPTEN__
return rand() % num_values;
#else
return arc4random_uniform(num_values);
#endif
}
static void _simon_clear_display(simon_state_t *state) {
if (state->playing_state == SIMON_NOT_PLAYING) {
watch_display_string(" ", 0);
} else {
sprintf(_simon_display_buf, " %2d ", state->sequence_length);
watch_display_string(_simon_display_buf, 0);
}
}
static void _simon_not_playing_display(simon_state_t *state) {
_simon_clear_display(state);
sprintf(_simon_display_buf, "SI %d", state->best_score);
if (!state->soundOff)
watch_set_indicator(WATCH_INDICATOR_BELL);
else
watch_clear_indicator(WATCH_INDICATOR_BELL);
if (!state->lightOff)
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
watch_display_string(_simon_display_buf, 0);
switch (state->mode)
{
case SIMON_MODE_EASY:
watch_display_string("E", 9);
break;
case SIMON_MODE_HARD:
watch_display_string("H", 9);
break;
default:
break;
}
}
static void _simon_reset(simon_state_t *state) {
state->playing_state = SIMON_NOT_PLAYING;
state->listen_index = 0;
state->sequence_length = 0;
_simon_not_playing_display(state);
}
static void _simon_display_note(SimonNote note, simon_state_t *state) {
char *ndtemplate = NULL;
switch (note) {
case SIMON_LED_NOTE:
ndtemplate = "LI%2d ";
break;
case SIMON_ALARM_NOTE:
ndtemplate = " %2d AL";
break;
case SIMON_MODE_NOTE:
ndtemplate = " %2dDE ";
break;
case SIMON_WRONG_NOTE:
ndtemplate = "OH NOOOOO";
}
sprintf(_simon_display_buf, ndtemplate, state->sequence_length);
watch_display_string(_simon_display_buf, 0);
}
static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_rest) {
_simon_display_note(note, state);
switch (note) {
case SIMON_LED_NOTE:
if (!state->lightOff) watch_set_led_yellow();
if (state->soundOff)
delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
break;
case SIMON_MODE_NOTE:
if (!state->lightOff) watch_set_led_red();
if (state->soundOff)
delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep);
break;
case SIMON_ALARM_NOTE:
if (!state->lightOff) watch_set_led_green();
if (state->soundOff)
delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep);
break;
case SIMON_WRONG_NOTE:
if (state->soundOff)
delay_ms(800);
else
watch_buzzer_play_note(BUZZER_NOTE_A1, 800);
break;
}
watch_set_led_off();
if (note != SIMON_WRONG_NOTE) {
_simon_clear_display(state);
if (!skip_rest) {
watch_buzzer_play_note(BUZZER_NOTE_REST, (_delay_beep * 2)/3);
}
}
}
static void _simon_setup_next_note(simon_state_t *state) {
if (state->sequence_length > state->best_score) {
state->best_score = state->sequence_length;
}
_simon_clear_display(state);
state->playing_state = SIMON_TEACHING;
state->sequence[state->sequence_length] = _simon_get_rand_num(3) + 1;
state->sequence_length = state->sequence_length + 1;
state->teaching_index = 0;
state->listen_index = 0;
}
static void _simon_listen(SimonNote note, simon_state_t *state) {
if (state->sequence[state->listen_index] == note) {
_simon_play_note(note, state, true);
state->listen_index++;
_timer = 0;
if (state->listen_index == state->sequence_length) {
state->playing_state = SIMON_READY_FOR_NEXT_NOTE;
}
} else {
_simon_play_note(SIMON_WRONG_NOTE, state, true);
_simon_reset(state);
}
}
static void _simon_begin_listening(simon_state_t *state) {
state->playing_state = SIMON_LISTENING_BACK;
state->listen_index = 0;
}
static void _simon_change_speed(simon_state_t *state){
switch (state->mode)
{
case SIMON_MODE_HARD:
_delay_beep = DELAY_FOR_TONE_MS / 2;
_secSub = SIMON_FACE_FREQUENCY / 2;
_timeout = (TIMER_MAX * SIMON_FACE_FREQUENCY) / 2;
break;
default:
_delay_beep = DELAY_FOR_TONE_MS;
_secSub = SIMON_FACE_FREQUENCY;
_timeout = TIMER_MAX * SIMON_FACE_FREQUENCY;
break;
}
}
void simon_face_setup(uint8_t watch_face_index,
void **context_ptr) {
(void)watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(simon_state_t));
memset(*context_ptr, 0, sizeof(simon_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 randon number generator
time_t t;
srand((unsigned)time(&t));
#endif
}
void simon_face_activate(void *context) {
(void) settings;
(void) context;
simon_state_t *state = (simon_state_t *)context;
_simon_change_speed(state);
movement_request_tick_frequency(SIMON_FACE_FREQUENCY);
_timer = 0;
}
bool simon_face_loop(movement_event_t event,
void *context) {
simon_state_t *state = (simon_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
// Show your initial UI here.
_simon_reset(state);
break;
case EVENT_TICK:
if (state->playing_state == SIMON_LISTENING_BACK && state->mode != SIMON_MODE_EASY)
{
_timer++;
if(_timer >= (_timeout)){
_timer = 0;
_simon_play_note(SIMON_WRONG_NOTE, state, true);
_simon_reset(state);
}
}
else if (state->playing_state == SIMON_TEACHING && event.subsecond == 0) {
SimonNote note = state->sequence[state->teaching_index];
// if this is the final note in the sequence, don't play the rest to let
// the player jump in faster
_simon_play_note(note, state, state->teaching_index == (state->sequence_length - 1));
state->teaching_index++;
if (state->teaching_index == state->sequence_length) {
_simon_begin_listening(state);
}
}
else if (state->playing_state == SIMON_READY_FOR_NEXT_NOTE && (event.subsecond % _secSub) == 0) {
_timer = 0;
_simon_setup_next_note(state);
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_LONG_PRESS:
if (state->playing_state == SIMON_NOT_PLAYING) {
state->lightOff = !state->lightOff;
_simon_not_playing_display(state);
}
break;
case EVENT_ALARM_LONG_PRESS:
if (state->playing_state == SIMON_NOT_PLAYING) {
state->soundOff = !state->soundOff;
_simon_not_playing_display(state);
if (!state->soundOff)
watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
}
break;
case EVENT_LIGHT_BUTTON_UP:
if (state->playing_state == SIMON_NOT_PLAYING) {
state->sequence_length = 0;
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
_simon_setup_next_note(state);
} else if (state->playing_state == SIMON_LISTENING_BACK) {
_simon_listen(SIMON_LED_NOTE, state);
}
break;
case EVENT_MODE_LONG_PRESS:
if (state->playing_state == SIMON_NOT_PLAYING) {
movement_move_to_face(0);
} else {
state->playing_state = SIMON_NOT_PLAYING;
_simon_reset(state);
}
break;
case EVENT_MODE_BUTTON_UP:
if (state->playing_state == SIMON_NOT_PLAYING) {
movement_move_to_next_face();
} else if (state->playing_state == SIMON_LISTENING_BACK) {
_simon_listen(SIMON_MODE_NOTE, state);
}
break;
case EVENT_ALARM_BUTTON_UP:
if (state->playing_state == SIMON_LISTENING_BACK) {
_simon_listen(SIMON_ALARM_NOTE, state);
}
else if (state->playing_state == SIMON_NOT_PLAYING){
state->mode = (state->mode + 1) % SIMON_MODE_TOTAL;
_simon_change_speed(state);
_simon_not_playing_display(state);
}
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
case EVENT_LOW_ENERGY_UPDATE:
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void simon_face_resign(void *context) {
(void)context;
watch_set_led_off();
watch_set_buzzer_off();
}

View File

@@ -0,0 +1,111 @@
/*
* MIT License
*
* Copyright (c) 2024 <#author_name#>
*
* 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 SIMON_FACE_H_
#define SIMON_FACE_H_
#include "movement.h"
/*
* simon_face
* -----------
* The classic electronic game, Simon, reduced to be played on a Sensor-Watch
*
* How to play:
*
* When first arriving at the face, it will show your best score.
*
* Press the light button to start the game.
*
* A sequence will be played, starting with length 1. The sequence can be
* made up of tones corresponding to any of the three buttons.
*
* light button: "LI" will display at the top of the screen, the LED will be yellow, and a high D will play
* mode button: "DE" will display at the left of the screen, the LED will be red, and a high E will play
* alarm button: "AL" will display on the right of the screen, the LED will be green, and a high C will play
*
* Once the sequence has finished, press the same buttons to recreate the sequence.
*
* If correct, the sequence will get one tone longer and play again. See how long of a sequence you can get.
*
* If you recreate the sequence incorrectly, a low note will play with "OH NOOOOO" displayed and the game is over.
* Press light to play again.
*
* Once playing, long press the mode button when it is your turn to exit the game early.
*/
#define MAX_SEQUENCE 99
typedef enum SimonNote {
SIMON_LED_NOTE = 1,
SIMON_MODE_NOTE,
SIMON_ALARM_NOTE,
SIMON_WRONG_NOTE
} SimonNote;
typedef enum SimonPlayingState {
SIMON_NOT_PLAYING = 0,
SIMON_TEACHING,
SIMON_LISTENING_BACK,
SIMON_READY_FOR_NEXT_NOTE
} SimonPlayingState;
typedef enum SimonMode {
SIMON_MODE_NORMAL = 0, // 5 Second timeout if nothing is input
SIMON_MODE_EASY, // There is no timeout in this mode
SIMON_MODE_HARD, // The speed of the teaching is doubled and th etimeout is halved
SIMON_MODE_TOTAL
} SimonMode;
typedef struct {
uint8_t best_score;
SimonNote sequence[MAX_SEQUENCE];
uint8_t sequence_length;
uint8_t teaching_index;
uint8_t listen_index;
bool soundOff;
bool lightOff;
uint8_t mode:6;
SimonPlayingState playing_state;
} simon_state_t;
void simon_face_setup(uint8_t watch_face_index, void **context_ptr);
void simon_face_activate(void *context);
bool simon_face_loop(movement_event_t event, void *context);
void simon_face_resign(void *context);
#define simon_face \
((const watch_face_t){ \
simon_face_setup, \
simon_face_activate, \
simon_face_loop, \
simon_face_resign, \
NULL, \
})
#define TIMER_MAX 5
#define SIMON_FACE_FREQUENCY 8
#define DELAY_FOR_TONE_MS 300
#endif // SIMON_FACE_H_

View File

@@ -0,0 +1,462 @@
/*
* MIT License
*
* Copyright (c) 2024 Patrick McGuire
*
* 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 <math.h>
#include "simple_calculator_face.h"
void simple_calculator_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(simple_calculator_state_t));
memset(*context_ptr, 0, sizeof(simple_calculator_state_t));
}
}
static void reset_to_zero(calculator_number_t *number) {
number->negative = false;
number->hundredths = 0;
number->tenths = 0;
number->ones = 0;
number->tens = 0;
number->hundreds = 0;
number->thousands = 0;
}
void simple_calculator_face_activate(void *context) {
simple_calculator_state_t *state = (simple_calculator_state_t *)context;
state->placeholder = PLACEHOLDER_ONES;
state->mode = MODE_ENTERING_FIRST_NUM;
reset_to_zero(&state->second_num);
reset_to_zero(&state->result);
movement_request_tick_frequency(4);
}
static void increment_placeholder(calculator_number_t *number, calculator_placeholder_t placeholder) {
uint8_t *digits[] = {
&number->hundredths,
&number->tenths,
&number->ones,
&number->tens,
&number->hundreds,
&number->thousands
};
*digits[placeholder] = (*digits[placeholder] + 1) % 10;
}
static float convert_to_float(calculator_number_t number) {
float result = 0.0;
// Add the whole number portion
result += number.thousands * 1000.0f;
result += number.hundreds * 100.0f;
result += number.tens * 10.0f;
result += number.ones * 1.0f;
// Add the fractional portion
result += number.tenths * 0.1f;
result += number.hundredths * 0.01f;
// Round to nearest hundredth
result = roundf(result * 100) / 100;
// Handle negative numbers
if (number.negative) result = -result;
//printf("convert_to_float results = %f\n", result); // For debugging
return result;
}
static char* update_display_number(calculator_number_t *number, char *display_string, uint8_t which_num) {
char sign = ' ';
if (number->negative) sign = '-';
sprintf(display_string, "CA%d%c%d%d%d%d%d%d",
which_num,
sign,
number->thousands,
number->hundreds,
number->tens,
number->ones,
number->tenths,
number->hundredths);
return display_string;
}
static void set_operation(simple_calculator_state_t *state) {
switch (state->operation) {
case OP_ADD:
watch_display_string(" Add", 0);
break;
case OP_SUB:
watch_display_string(" sub", 0);
break;
case OP_MULT:
watch_display_string(" n&ul", 0);
break;
case OP_DIV:
watch_display_string(" div", 0);
break;
case OP_ROOT:
watch_display_string(" root", 0);
break;
case OP_POWER:
watch_display_string(" pow", 0);
break;
}
}
static void cycle_operation(simple_calculator_state_t *state) {
state->operation = (state->operation + 1) % OPERATIONS_COUNT; // Assuming there are 6 operations
}
static calculator_number_t convert_to_string(float number) {
calculator_number_t result;
// Handle negative numbers
if (number < 0) {
number = -number;
result.negative = true;
} else result.negative = false;
// Get each digit from each placeholder
int int_part = (int)number;
float decimal_part_float = ((number - int_part) * 100); // two decimal places
//printf("decimal_part_float = %f\n", decimal_part_float); //For debugging
int decimal_part = round(decimal_part_float);
//printf("decimal_part = %d\n", decimal_part); //For debugging
result.thousands = int_part / 1000 % 10;
result.hundreds = int_part / 100 % 10;
result.tens = int_part / 10 % 10;
result.ones = int_part % 10;
result.tenths = decimal_part / 10 % 10;
result.hundredths = decimal_part % 10;
return result;
}
// This is the main function for setting the first_num and second_num
// WISH: there must be a way to pass less to this function?
static void set_number(calculator_number_t *number, calculator_placeholder_t placeholder, char *display_string, char *temp_display_string, movement_event_t event, uint8_t which_num) {
// Create the display index
uint8_t display_index;
// Update display string with current number and copy into temp string
update_display_number(number, display_string, which_num);
strcpy(temp_display_string, display_string);
// Determine the display index based on the placeholder
display_index = 9 - placeholder;
// Blink selected placeholder
// Check if `event.subsecond` is even
if (event.subsecond % 2 == 0) {
// Replace the character at the index corresponding to the current placeholder with a space
temp_display_string[display_index] = ' ';
}
// Display the (possibly modified) string
watch_display_string(temp_display_string, 0);
}
static void view_results(simple_calculator_state_t *state, char *display_string) {
// Initialize float variables to do the math
float first_num_float, second_num_float, result_float = 0.0f;
// Convert the passed numbers to floats
first_num_float = convert_to_float(state->first_num);
second_num_float = convert_to_float(state->second_num);
// Perform the calculation based on the selected operation
switch (state->operation) {
case OP_ADD:
result_float = first_num_float + second_num_float;
break;
case OP_SUB:
result_float = first_num_float - second_num_float;
break;
case OP_MULT:
result_float = first_num_float * second_num_float;
break;
case OP_DIV:
if (second_num_float != 0) {
result_float = first_num_float / second_num_float;
} else {
state->mode = MODE_ERROR;
return;
}
break;
case OP_ROOT:
if (first_num_float >= 0) {
result_float = sqrtf(first_num_float);
} else {
state->mode = MODE_ERROR;
return;
}
break;
case OP_POWER:
result_float = powf(first_num_float, second_num_float);
break;
default:
result_float = 0.0f;
break;
}
// Be sure the result can fit on the watch display, else error
if (result_float > 9999.99 || result_float < -9999.99) {
state->mode = MODE_ERROR;
return;
}
result_float = roundf(result_float * 100.0f) / 100.0f; // Might not be needed
//printf("result as float = %f\n", result_float); // For debugging
// Convert the float result to a string
// This isn't strictly necessary, but allows easily reusing the result as
// the next calculation's first_num
state->result = convert_to_string(result_float);
// Update the display with the result
update_display_number(&state->result, display_string, 3);
//printf("display_string = %s\n", display_string); // For debugging
watch_display_string(display_string, 0);
}
// Used both when returning from errors and when long pressing MODE
static void reset_all(simple_calculator_state_t *state) {
reset_to_zero(&state->first_num);
reset_to_zero(&state->second_num);
state->mode = MODE_ENTERING_FIRST_NUM;
state->operation = OP_ADD;
state->placeholder = PLACEHOLDER_ONES;
}
bool simple_calculator_face_loop(movement_event_t event, void *context) {
simple_calculator_state_t *state = (simple_calculator_state_t *)context;
char display_string[10];
char temp_display_string[10]; // Temporary buffer for blinking effect
switch (event.event_type) {
case EVENT_ACTIVATE:
case EVENT_TICK:
switch (state->mode) {
case MODE_ENTERING_FIRST_NUM:
// See the WISH for this function above
set_number(&state->first_num,
state->placeholder,
display_string,
temp_display_string,
event,
1);
break;
case MODE_CHOOSING:
set_operation(state);
break;
case MODE_ENTERING_SECOND_NUM:
// If doing a square root calculation, skip to results
if (state->operation == OP_ROOT) {
state->mode = MODE_VIEW_RESULTS;
} else {
// See the WISH for this function above
set_number(&state->second_num,
state->placeholder,
display_string,
temp_display_string,
event,
2);
}
break;
case MODE_VIEW_RESULTS:
view_results(state, display_string);
break;
case MODE_ERROR:
watch_display_string("CA Error ", 0);
break;
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_BUTTON_UP:
switch (state->mode) {
case MODE_ENTERING_FIRST_NUM:
case MODE_ENTERING_SECOND_NUM:
// Move to the next placeholder when the light button is pressed
state->placeholder = (state->placeholder + 1) % MAX_PLACEHOLDERS; // Loop back to the start after PLACEHOLDER_THOUSANDS
break;
case MODE_CHOOSING:
cycle_operation(state);
break;
case MODE_ERROR:
reset_all(state);
break;
case MODE_VIEW_RESULTS:
break;
}
break;
case EVENT_LIGHT_LONG_PRESS:
switch (state->mode) {
case MODE_ENTERING_FIRST_NUM:
// toggle negative on state->first_num
state->first_num.negative = !state->first_num.negative;
break;
case MODE_ENTERING_SECOND_NUM:
// toggle negative on state->second_num
state->second_num.negative = !state->second_num.negative;
break;
case MODE_ERROR:
reset_all(state);
break;
case MODE_CHOOSING:
case MODE_VIEW_RESULTS:
break;
}
break;
case EVENT_ALARM_BUTTON_UP:
switch (state->mode) {
case MODE_ENTERING_FIRST_NUM:
// Increment the digit in the current placeholder
increment_placeholder(&state->first_num, state->placeholder);
update_display_number(&state->first_num, display_string, 1);
//printf("display_string = %s\n", display_string); // For debugging
break;
case MODE_CHOOSING:
// Confirm and select the current operation
state->mode = MODE_ENTERING_SECOND_NUM;
break;
case MODE_ENTERING_SECOND_NUM:
// Increment the digit in the current placeholder
increment_placeholder(&state->second_num, state->placeholder);
update_display_number(&state->second_num, display_string, 2);
//printf("display_string = %s\n", display_string); // For debugging
break;
case MODE_ERROR:
reset_all(state);
break;
case MODE_VIEW_RESULTS:
break;
}
break;
case EVENT_ALARM_LONG_PRESS:
switch (state->mode) {
case MODE_ENTERING_FIRST_NUM:
reset_to_zero(&state->first_num);
break;
case MODE_ENTERING_SECOND_NUM:
reset_to_zero(&state->second_num);
break;
case MODE_ERROR:
reset_all(state);
break;
case MODE_CHOOSING:
case MODE_VIEW_RESULTS:
break;
}
break;
case EVENT_MODE_BUTTON_DOWN:
break;
case EVENT_MODE_BUTTON_UP:
if (state->mode == MODE_ERROR) {
reset_all(state);
} else if (state->mode == MODE_ENTERING_FIRST_NUM &&
state->first_num.hundredths == 0 &&
state->first_num.tenths == 0 &&
state->first_num.ones== 0 &&
state->first_num.tens == 0 &&
state->first_num.hundreds == 0 &&
state->first_num.thousands == 0) {
movement_move_to_next_face();
} else {
// Reset the placeholder and proceed to the next MODE
state->placeholder = PLACEHOLDER_ONES;
state->mode = (state->mode + 1) % 4;
// When looping back to MODE_ENTERING_FIRST_NUM, reuse the
// previous calculation's results as the next calculation's
// first_num; also reset other numbers
if (state->mode == MODE_ENTERING_FIRST_NUM) {
state->first_num = state->result;
reset_to_zero(&state->second_num);
reset_to_zero(&state->result);
}
}
break;
case EVENT_MODE_LONG_PRESS:
// Move to next face if first number is 0
if (state->first_num.hundredths == 0 &&
state->first_num.tenths == 0 &&
state->first_num.ones== 0 &&
state->first_num.tens == 0 &&
state->first_num.hundreds == 0 &&
state->first_num.thousands == 0) {
movement_move_to_face(0);
// otherwise, start over
} else {
reset_all(state);
}
break;
case EVENT_TIMEOUT:
movement_request_tick_frequency(1);
movement_move_to_face(0);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void simple_calculator_face_resign(void *context) {
(void) context;
movement_request_tick_frequency(1);
}

View File

@@ -0,0 +1,145 @@
/*
* MIT License
*
* Copyright (c) 2024 Patrick McGuire
*
* 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 SIMPLE_CALCULATOR_FACE_H_
#define SIMPLE_CALCULATOR_FACE_H_
#include "movement.h"
/*
* Simple Calculator
*
* How to use:
*
* Flow:
* Enter first number -> Select operator -> Enter second number -> View Results
*
* How to read the display:
* - "CA" is displayed at the top to tell you that you're in the CAlculator
* - The top-right digit (1, 2, or 3) lets you know whether you're entering the
* first number (1), entering the second number (2), or viewing the results (3).
* - To the right of the top-right digit will show the number's sign. If the
* number is negative, a "-" will be displayed, otherwise it is empty.
* - The 4 large digits to the left are whole numbers and the 2 smaller digits
* on the right are the tenths and hundredths decimal places.
*
* Entering the first number:
* - Press ALARM to increment the selected (blinking) digit
* - Press LIGHT to move to the next placeholder
* - LONG PRESS the LIGHT button to toggle the number's sign to make it
* negative
* - LONG PRESS the ALARM button to reset the number to 0
* - Press MODE to proceed to selecting the operator
*
* Selecting the operator:
* - Press the LIGHT button to cycle through available operators. They are:
* + Add
* - Subtract
* * Multiply
* / Divide
* sqrtf() Square root
* powf() Power (exponent calculation)
* - Press MODE or ALARM to proceed to entering the second number
*
* Entering the second number:
* - Everything is the same as setting the first number except that pressing
* MODE here will proceed to viewing the results
*
* Viewing the results:
* - Pressing MODE will start a new calculation with the result as the first
* number. (LONG PRESS ALARM to reset the value to 0)
*
* Errors:
* - An error will be triggered if the result is not able to be displayed, that
* is, if the value is greater than 9,999.99 or less than -9,999.99.
* - An error will also be triggered if an impossible operation is selected,
* for instance trying to divide by 0 or get the square root of a negative
* number.
* - Exit error mode and start over with any button press.
*
*/
#define OPERATIONS_COUNT 6
#define MAX_PLACEHOLDERS 6
typedef struct {
bool negative;
uint8_t hundredths;
uint8_t tenths;
uint8_t ones;
uint8_t tens;
uint8_t hundreds;
uint8_t thousands;
} calculator_number_t;
typedef enum {
PLACEHOLDER_HUNDREDTHS,
PLACEHOLDER_TENTHS,
PLACEHOLDER_ONES,
PLACEHOLDER_TENS,
PLACEHOLDER_HUNDREDS,
PLACEHOLDER_THOUSANDS
} calculator_placeholder_t;
typedef enum {
OP_ADD,
OP_SUB,
OP_MULT,
OP_DIV,
OP_ROOT,
OP_POWER,
} calculator_operation_t;
typedef enum {
MODE_ENTERING_FIRST_NUM,
MODE_CHOOSING,
MODE_ENTERING_SECOND_NUM,
MODE_VIEW_RESULTS,
MODE_ERROR
} calculator_mode_t;
typedef struct {
calculator_number_t first_num;
calculator_number_t second_num;
calculator_number_t result;
calculator_operation_t operation;
calculator_mode_t mode;
calculator_placeholder_t placeholder;
} simple_calculator_state_t;
void simple_calculator_face_setup(uint8_t watch_face_index, void ** context_ptr);
void simple_calculator_face_activate(void *context);
bool simple_calculator_face_loop(movement_event_t event, void *context);
void simple_calculator_face_resign(void *context);
#define simple_calculator_face ((const watch_face_t){ \
simple_calculator_face_setup, \
simple_calculator_face_activate, \
simple_calculator_face_loop, \
simple_calculator_face_resign, \
NULL, \
})
#endif // SIMPLE_CALCULATOR_FACE_H_

View File

@@ -0,0 +1,136 @@
/*
* MIT License
*
* Copyright (c) 2023 Wesley Aptekar-Cassels
*
* 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.
*/
#if __EMSCRIPTEN__
#include <time.h>
#else
#include "saml22j18a.h"
#endif
#include <stdlib.h>
#include <string.h>
#include "simple_coin_flip_face.h"
#define SIMPLE_COIN_FLIP_REQUIRE_LONG_PRESS_FOR_REFLIP true
void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(simple_coin_flip_state_t));
memset(*context_ptr, 0, sizeof(simple_coin_flip_state_t));
}
}
void simple_coin_flip_face_activate(void *context) {
(void) context;
}
static uint32_t get_random(uint32_t max) {
#if __EMSCRIPTEN__
return rand() % max;
#else
return arc4random_uniform(max);
#endif
}
static void animation_0() {
watch_display_string(" ", 8);
watch_set_pixel(0, 3);
watch_set_pixel(0, 6);
}
static void animation_1() {
watch_display_string(" ", 8);
watch_set_pixel(1, 3);
watch_set_pixel(1, 5);
}
static void animation_2() {
watch_display_string(" ", 8);
watch_set_pixel(2, 2);
watch_set_pixel(2, 4);
}
bool simple_coin_flip_face_loop(movement_event_t event, void *context) {
simple_coin_flip_state_t *state = (simple_coin_flip_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
watch_display_string("flip", 5);
state->animation_frame = 0;
break;
case EVENT_TICK:
switch (state->animation_frame) {
case 0:
case 7:
return true;
case 1:
movement_request_tick_frequency(8);
watch_display_string(" ", 4);
// fallthrough
case 5:
animation_0();
break;
case 2:
case 4:
animation_1();
break;
case 3:
animation_2();
break;
case 6:
movement_request_tick_frequency(1);
if (get_random(2)) {
watch_display_string("Heads ", 4);
} else {
watch_display_string(" Tails", 4);
}
break;
}
state->animation_frame++;
break;
case EVENT_LIGHT_BUTTON_UP:
case EVENT_ALARM_BUTTON_UP:
if (!SIMPLE_COIN_FLIP_REQUIRE_LONG_PRESS_FOR_REFLIP || state->animation_frame == 0) {
state->animation_frame = 1;
}
break;
case EVENT_ALARM_LONG_PRESS:
case EVENT_LIGHT_LONG_PRESS:
state->animation_frame = 1;
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void simple_coin_flip_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,62 @@
/*
* MIT License
*
* Copyright (c) 2023 Wesley Aptekar-Cassels
*
* 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 SIMPLE_COIN_FLIP_FACE_H_
#define SIMPLE_COIN_FLIP_FACE_H_
#include "movement.h"
/*
* A extremely simple coin flip face.
*
* Press ALARM or LIGHT to flip a coin, after a short animation it will display
* "Heads" or "Tails". Long-press to flip again (you can change a #define to
* allow a short-press to reflip as well, if you'd like).
*
* This is for people who want a simpler UI than probability_face or
* randonaut_face. While those have more features, this one is more immediately
* obvious - useful, for instance, if you are using a coin flip to agree on
* something with someone, and want the operation to be clear to someone who
* has not had anything explained to them.
*/
typedef struct {
uint8_t animation_frame;
} simple_coin_flip_state_t;
void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr);
void simple_coin_flip_face_activate(void *context);
bool simple_coin_flip_face_loop(movement_event_t event, void *context);
void simple_coin_flip_face_resign(void *context);
#define simple_coin_flip_face ((const watch_face_t){ \
simple_coin_flip_face_setup, \
simple_coin_flip_face_activate, \
simple_coin_flip_face_loop, \
simple_coin_flip_face_resign, \
NULL, \
})
#endif // SIMPLE_COIN_FLIP_FACE_H_

View File

@@ -0,0 +1,500 @@
/*
* 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(uint8_t watch_face_index, void ** context_ptr) {
(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(void *context) {
(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, void *context) {
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);
break;
}
return true;
}
void smallchess_face_resign(void *context) {
(void) context;
watch_set_led_off();
}

View 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(uint8_t watch_face_index, void ** context_ptr);
void smallchess_face_activate(void *context);
bool smallchess_face_loop(movement_event_t event, void *context);
void smallchess_face_resign(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_

View File

@@ -0,0 +1,233 @@
/*
* MIT License
*
* Copyright (c) 2023 Wesley Aptekar-Cassels
*
* 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 <math.h>
#include "watch_utility.h"
#include "solstice_face.h"
// Find solstice or equinox time in JDE for a given year, via method from Meeus Ch 27
static double calculate_solstice_equinox(uint16_t year, uint8_t k) {
double Y = ((double)year - 2000) / 1000;
double approx_terms[4][5] = {
{2451623.80984, 365242.37404, 0.05169, -0.00411, -0.00057}, // March equinox
{2451716.56767, 365241.62603, 0.00325, 0.00888, -0.00030}, // June solstice
{2451810.21715, 365242.01767, -0.11575, 0.00337, 0.00078}, // September equinox
{2451900.05952, 365242.74049, -0.06223, -0.00823, 0.00032}, // December solstice
};
double JDE0 = approx_terms[k][0] + Y * (approx_terms[k][1] + Y * (approx_terms[k][2] + Y * (approx_terms[k][3] + Y * approx_terms[k][4])));
double T = (JDE0 - 2451545.0) / 36525;
double W = 35999.373 * T - 2.47;
double dlambda = 1 + (0.0334 * cos(W * M_PI / 180.0)) + (0.0007 * cos(2 * W * M_PI / 180.0));
double correction_terms[25][3] = {
{485,324.96,1934.136},
{203,337.23,32964.467},
{199,342.08,20.186},
{182,27.85,445267.112},
{156,73.14,45036.886},
{136,171.52,22518.443},
{77,222.54,65928.934},
{74,296.72,3034.906},
{70,243.58,9037.513},
{58,119.81,33718.147},
{52,297.17,150.678},
{50,21.02,2281.226},
{45,247.54,29929.562},
{44,325.15,31555.956},
{29,60.93,4443.417},
{18,155.12,67555.328},
{17,288.79,4562.452},
{16,198.04,62894.029},
{14,199.76,31436.921},
{12,95.39,14577.848},
{12,287.11,31931.756},
{12,320.81,34777.259},
{9,227.73,1222.114},
{8,15.45,16859.074},
};
double S = 0;
for (int i = 0; i < 25; i++) {
S += correction_terms[i][0] * cos((correction_terms[i][1] + correction_terms[i][2] * T) * M_PI / 180.0);
}
double JDE = JDE0 + (0.00001 * S) / dlambda;
return JDE;
}
// Convert JDE to Gergorian datetime as per Meeus Ch 7
static watch_date_time_t jde_to_date_time(double JDE) {
double tmp = JDE + 0.5;
double Z = floor(tmp);
double F = fmod(tmp, 1);
double A;
if (Z < 2299161) {
A = Z;
} else {
double alpha = floor((Z - 1867216.25) / 36524.25);
A = Z + 1 + alpha - floor(alpha / 4);
}
double B = A + 1524;
double C = floor((B - 122.1) / 365.25);
double D = floor(365.25 * C);
double E = floor((B - D) / 30.6001);
double day = B - D - floor(30.6001 * E) + F;
double month;
if (E < 14) {
month = E - 1;
} else {
month = E - 13;
}
double year;
if (month > 2) {
year = C - 4716;
} else {
year = C - 4715;
}
double hours = fmod(day, 1) * 24;
double minutes = fmod(hours, 1) * 60;
double seconds = fmod(minutes, 1) * 60;
watch_date_time_t result = {.unit = {
floor(seconds),
floor(minutes),
floor(hours),
floor(day),
floor(month),
floor(year - 2020)
}};
return result;
}
static void calculate_datetimes(solstice_state_t *state) {
for (int i = 0; i < 4; i++) {
// TODO: handle DST changes
state->datetimes[i] = jde_to_date_time(calculate_solstice_equinox(2020 + state->year, i) + (movement_get_current_timezone_offset() / (3600.0*24.0)));
}
}
void solstice_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(solstice_state_t));
solstice_state_t *state = (solstice_state_t *)*context_ptr;
watch_date_time_t now = watch_rtc_get_date_time();
state->year = now.unit.year;
state->index = 0;
calculate_datetimes(state, settings);
uint32_t now_unix = watch_utility_date_time_to_unix_time(now, 0);
for (int i = 0; i < 4; i++) {
if (state->index == 0 && watch_utility_date_time_to_unix_time(state->datetimes[i], 0) > now_unix) {
state->index = i;
}
}
}
}
void solstice_face_activate(void *context) {
(void) context;
}
static void show_main_screen(solstice_state_t *state) {
char buf[11];
watch_date_time_t date_time = state->datetimes[state->index];
sprintf(buf, " %2d %2d%02d", date_time.unit.year + 20, date_time.unit.month, date_time.unit.day);
watch_display_string(buf, 0);
}
static void show_date_time(solstice_state_t *state) {
char buf[11];
watch_date_time_t date_time = state->datetimes[state->index];
if (!movement_clock_mode_24h()) {
if (date_time.unit.hour < 12) {
watch_clear_indicator(WATCH_INDICATOR_PM);
} else {
watch_set_indicator(WATCH_INDICATOR_PM);
}
date_time.unit.hour %= 12;
if (date_time.unit.hour == 0) date_time.unit.hour = 12;
}
sprintf(buf, "%s%2d%2d%02d%02d", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
watch_set_colon();
watch_display_string(buf, 0);
}
bool solstice_face_loop(movement_event_t event, void *context) {
solstice_state_t *state = (solstice_state_t *)context;
switch (event.event_type) {
case EVENT_ALARM_LONG_PRESS:
show_date_time(state);
break;
case EVENT_LIGHT_BUTTON_UP:
if (state->index == 0) {
if (state->year == 0) {
break;
}
state->year--;
state->index = 3;
calculate_datetimes(state, settings);
} else {
state->index--;
}
show_main_screen(state);
break;
case EVENT_ALARM_BUTTON_UP:
state->index++;
if (state->index > 3) {
if (state->year == 83) {
break;
}
state->year++;
state->index = 0;
calculate_datetimes(state, settings);
}
show_main_screen(state);
break;
case EVENT_ALARM_LONG_UP:
watch_clear_colon();
watch_clear_indicator(WATCH_INDICATOR_PM);
show_main_screen(state);
break;
case EVENT_ACTIVATE:
show_main_screen(state);
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void solstice_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,64 @@
/*
* MIT License
*
* Copyright (c) 2023 Wesley Aptekar-Cassels
*
* 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 SOLSTICE_FACE_H_
#define SOLSTICE_FACE_H_
#include "movement.h"
/*
* A face for telling the dates and times of solstices and equinoxes
*
* It shows the upcoming solstice or equinox by default. The upper right number
* is the year, and the bottom numbers are the date in MMDD format. Use the
* alarm / light buttons to go forwards / backwards in time. Long press the
* alarm button to show the time of the event, including what weekday it is on,
* in your local timezone (DST is not handled).
*
* Supports the years 2020 - 2083. The calculations are reasonably accurate for
* years between 1000 and 3000, but limitations in the sensor watch libraries
* (which could easily be worked around) prevent making use of that.
*/
typedef struct {
watch_date_time_t datetimes[4];
uint8_t year;
uint8_t index;
} solstice_state_t;
void solstice_face_setup(uint8_t watch_face_index, void ** context_ptr);
void solstice_face_activate(void *context);
bool solstice_face_loop(movement_event_t event, void *context);
void solstice_face_resign(void *context);
#define solstice_face ((const watch_face_t){ \
solstice_face_setup, \
solstice_face_activate, \
solstice_face_loop, \
solstice_face_resign, \
NULL, \
})
#endif // SOLSTICE_FACE_H_

View File

@@ -0,0 +1,171 @@
/*
* MIT License
*
* Copyright (c) 2022 Wesley Ellis
* 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 "stopwatch_face.h"
#include "watch.h"
#include "watch_utility.h"
// distant future for background task: January 1, 2083
// see stopwatch_face_activate for details
static const watch_date_time_t distant_future = {
.unit = {0, 0, 0, 1, 1, 63}
};
void stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(stopwatch_state_t));
memset(*context_ptr, 0, sizeof(stopwatch_state_t));
}
}
static void _stopwatch_face_update_display(stopwatch_state_t *stopwatch_state, bool show_seconds) {
if (stopwatch_state->running) {
watch_date_time_t now = watch_rtc_get_date_time();
uint32_t now_timestamp = watch_utility_date_time_to_unix_time(now, 0);
uint32_t start_timestamp = watch_utility_date_time_to_unix_time(stopwatch_state->start_time, 0);
stopwatch_state->seconds_counted = now_timestamp - start_timestamp;
}
if (stopwatch_state->seconds_counted >= 3456000) {
// display maxes out just shy of 40 days, thanks to the limit on the day digits (0-39)
stopwatch_state->running = false;
movement_cancel_background_task();
watch_display_string("st39235959", 0);
return;
}
watch_duration_t duration = watch_utility_seconds_to_duration(stopwatch_state->seconds_counted);
char buf[14];
sprintf(buf, "st %02d%02d ", duration.hours, duration.minutes);
watch_display_string(buf, 0);
if (duration.days != 0) {
sprintf(buf, "%2d", (uint8_t)duration.days);
watch_display_string(buf, 2);
}
if (show_seconds) {
sprintf(buf, "%02d", duration.seconds);
watch_display_string(buf, 8);
}
}
void stopwatch_face_activate(void *context) {
if (watch_sleep_animation_is_running()) watch_stop_sleep_animation();
stopwatch_state_t *stopwatch_state = (stopwatch_state_t *)context;
if (stopwatch_state->running) {
// because the low power update happens on the minute mark, and the wearer could start
// the stopwatch anytime, the low power update could fire up to 59 seconds later than
// we need it to, causing the stopwatch to display stale data.
// So let's schedule a background task that will never fire. This will keep the watch
// from entering low energy mode while the stopwatch is on screen. This background task
// will remain scheduled until the stopwatch stops OR this watch face resigns.
movement_schedule_background_task(distant_future);
}
}
bool stopwatch_face_loop(movement_event_t event, void *context) {
stopwatch_state_t *stopwatch_state = (stopwatch_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
watch_set_colon();
// fall through
case EVENT_TICK:
if (stopwatch_state->start_time.reg == 0) {
watch_display_string("st 000000", 0);
} else {
_stopwatch_face_update_display(stopwatch_state, true);
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
movement_illuminate_led();
if (!stopwatch_state->running) {
stopwatch_state->start_time.reg = 0;
stopwatch_state->seconds_counted = 0;
watch_display_string("st 000000", 0);
}
break;
case EVENT_ALARM_BUTTON_DOWN:
if (movement_button_should_sound()) {
watch_buzzer_play_note(BUZZER_NOTE_C8, 50);
}
stopwatch_state->running = !stopwatch_state->running;
if (stopwatch_state->running) {
// we're running now, so we need to set the start_time.
if (stopwatch_state->start_time.reg == 0) {
// if starting from the reset state, easy: we start now.
stopwatch_state->start_time = watch_rtc_get_date_time();
} else {
// if resuming with time already on the clock, the original start time isn't valid anymore!
// so let's fetch the current time...
uint32_t timestamp = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0);
// ...subtract the seconds we've already counted...
timestamp -= stopwatch_state->seconds_counted;
// and resume from the "virtual" start time that's that many seconds ago.
stopwatch_state->start_time = watch_utility_date_time_from_unix_time(timestamp, 0);
}
// schedule our keepalive task when running...
movement_schedule_background_task(distant_future);
} else {
// and cancel it when stopped.
movement_cancel_background_task();
}
break;
case EVENT_TIMEOUT:
// explicitly ignore the timeout event so we stay on screen
break;
case EVENT_LOW_ENERGY_UPDATE:
if (!watch_sleep_animation_is_running()) watch_start_sleep_animation(1000);
if (!stopwatch_state->running) {
// since the tick animation is running, displaying the stopped time could be misleading,
// as it could imply that the stopwatch is running. instead, show a blank display to
// indicate that we are in sleep mode.
watch_display_string("st ---- ", 0);
} else {
// this OTOH shouldn't happen anymore; if we're running, we shouldn't enter low energy mode
_stopwatch_face_update_display(stopwatch_state, false);
watch_set_indicator(WATCH_INDICATOR_BELL);
}
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void stopwatch_face_resign(void *context) {
(void) context;
// regardless of whether we're running or stopped, cancel the task
// that was keeping us awake while on screen.
movement_cancel_background_task();
}

View File

@@ -0,0 +1,61 @@
/*
* MIT License
*
* Copyright (c) 2022 Wesley Ellis
* 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 STOPWATCH_FACE_H_
#define STOPWATCH_FACE_H_
/*
* STOPWATCH FACE
*
* The Stopwatch face provides basic stopwatch functionality: you can start
* and stop the stopwatch with the alarm button. Pressing the light button
* when the timer is stopped resets it.
*
* This face does not count sub-seconds.
* See also: "fast_stopwatch_face.h"
*/
#include "movement.h"
typedef struct {
bool running;
watch_date_time_t start_time; // while running, show the difference between this time and now
uint32_t seconds_counted; // set this value when paused, and show that instead.
} stopwatch_state_t;
void stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr);
void stopwatch_face_activate(void *context);
bool stopwatch_face_loop(movement_event_t event, void *context);
void stopwatch_face_resign(void *context);
#define stopwatch_face ((const watch_face_t){ \
stopwatch_face_setup, \
stopwatch_face_activate, \
stopwatch_face_loop, \
stopwatch_face_resign, \
NULL, \
})
#endif // STOPWATCH_FACE_H_

View File

@@ -0,0 +1,265 @@
/*
* MIT License
*
* Copyright (c) 2022 Raymundo Cassani
*
* 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 "tachymeter_face.h"
#include "watch_utility.h"
static uint32_t _distance_from_struct(distance_digits_t dist_digits) {
// distance from digitwise distance
uint32_t retval = (dist_digits.thousands * 100000 +
dist_digits.hundreds * 10000 +
dist_digits.tens * 1000 +
dist_digits.ones * 100 +
dist_digits.tenths * 10 +
dist_digits.hundredths);// * 1
return retval;
}
void tachymeter_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void)watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(tachymeter_state_t));
memset(*context_ptr, 0, sizeof(tachymeter_state_t));
tachymeter_state_t *state = (tachymeter_state_t *)*context_ptr;
// Default distance
state->dist_digits.ones = 1;
state->distance = _distance_from_struct(state->dist_digits);
}
}
void tachymeter_face_activate(void *context) {
(void)context;
movement_request_tick_frequency(4); // 4Hz
}
static void _tachymeter_face_distance_lcd(movement_event_t event, tachymeter_state_t *state){
char buf[11];
// Distance from digits
state->distance = _distance_from_struct(state->dist_digits);
sprintf(buf, "TC %c%06lu", state->running ? ' ' : 'd', state->distance);
// Blinking display when editing
if (state->editing) {
// Blink 'd'
if (event.subsecond < 2) {
buf[3] = ' ';
}
// Blink active digit
if (event.subsecond % 2) {
buf[state->active_digit + 4] = ' ';
}
}
watch_display_string(buf, 0);
}
static void _tachymeter_face_totals_lcd(tachymeter_state_t *state, bool show_time){
char buf[15];
if (!show_time){
sprintf(buf, "TC %c%6lu", 'h', state->total_speed);
} else {
sprintf(buf, "TC %c%6lu", 't', state->total_time);
}
watch_display_string(buf, 0);
if (!show_time){
// Show '/' besides 'H'
watch_set_pixel(0, 9);
watch_set_pixel(0, 10);
}
}
bool tachymeter_face_loop(movement_event_t event, void *context) {
tachymeter_state_t *state = (tachymeter_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
// Show distance in UI
if (state->total_time == 0) {
_tachymeter_face_distance_lcd(event, state);
}
break;
case EVENT_TICK:
// Show editing distance (blinking)
if (state->editing) {
_tachymeter_face_distance_lcd(event, state);
}
if (!state->running && state->total_time != 0) {
// Display results if finished and not cleared
if (event.subsecond < 2) {
_tachymeter_face_totals_lcd(state, true);
} else {
_tachymeter_face_totals_lcd(state, false);
}
} else if (state->running){
watch_display_string(" ", 2);
switch (state->animation_state) {
case 0:
watch_set_pixel(0, 7);
break;
case 1:
watch_set_pixel(1, 7);
break;
case 2:
watch_set_pixel(2, 7);
break;
case 3:
watch_set_pixel(2, 6);
break;
case 4:
watch_set_pixel(2, 8);
break;
case 5:
watch_set_pixel(0, 8);
break;
}
state->animation_state = (state->animation_state + 1) % 6;
}
break;
case EVENT_LIGHT_BUTTON_UP:
if (state->editing){
// Go to next digit
state->active_digit = (state->active_digit + 1) % 6;
} else {
movement_illuminate_led();
}
break;
case EVENT_LIGHT_LONG_PRESS:
if (!state->running && !state->editing){
if (state->total_time != 0){
// Clear results
state->total_time = 0;
state->total_speed = 0;
} else {
// Default distance
state->dist_digits.thousands = 0;
state->dist_digits.hundreds = 0;
state->dist_digits.tens = 0;
state->dist_digits.ones = 1;
state->dist_digits.tenths = 0;
state->dist_digits.hundredths = 0;
state->distance = _distance_from_struct(state->dist_digits);
}
_tachymeter_face_distance_lcd(event, state);
}
break;
case EVENT_ALARM_BUTTON_UP:
if (!state->running && state->total_time == 0){
if (movement_button_should_sound() && !state->editing) {
watch_buzzer_play_note(BUZZER_NOTE_C8, 50);
}
if (!state->editing) {
// Start running
state->running = true;
state->start_seconds = watch_rtc_get_date_time();
state->start_subsecond = event.subsecond;
state->total_time = 0;
} else {
// Alarm button to increase active digit
switch (state->active_digit) {
case 0:
state->dist_digits.thousands = (state->dist_digits.thousands + 1) % 10;
break;
case 1:
state->dist_digits.hundreds = (state->dist_digits.hundreds + 1) % 10;
break;
case 2:
state->dist_digits.tens = (state->dist_digits.tens + 1) % 10;
break;
case 3:
state->dist_digits.ones = (state->dist_digits.ones + 1) % 10;
break;
case 4:
state->dist_digits.tenths = (state->dist_digits.tenths + 1) % 10;
break;
case 5:
state->dist_digits.hundredths = (state->dist_digits.hundredths + 1) % 10;
break;
}
}
} else if (state->running) {
if (movement_button_should_sound() && !state->editing) {
watch_buzzer_play_note(BUZZER_NOTE_C8, 50);
}
// Stop running
state->running = false;
watch_date_time_t now = watch_rtc_get_date_time();
uint32_t now_timestamp = watch_utility_date_time_to_unix_time(now, 0);
uint32_t start_timestamp = watch_utility_date_time_to_unix_time(state->start_seconds, 0);
// Total time in centiseconds
state->total_time = ((now_timestamp*100) + (event.subsecond*25)) - ((start_timestamp*100) + (state->start_subsecond*25));
// Total speed in distance units per hour
state->total_speed = (uint32_t)(3600 * 100 * state->distance / state->total_time);
}
break;
case EVENT_ALARM_LONG_PRESS:
if (!state->running && state->total_time == 0){
if (!state->editing) {
// Enter editing
state->editing = true;
state->active_digit = 0;
if (movement_button_should_sound()) {
watch_buzzer_play_note(BUZZER_NOTE_C7, 80);
watch_buzzer_play_note(BUZZER_NOTE_C8, 80);
}
} else {
// Exit editing
state->editing = false;
// Validate distance
if(_distance_from_struct(state->dist_digits) == 0){
state->dist_digits.ones = 1;
}
_tachymeter_face_distance_lcd(event, state);
if (movement_button_should_sound()) {
watch_buzzer_play_note(BUZZER_NOTE_C8, 80);
watch_buzzer_play_note(BUZZER_NOTE_C7, 80);
}
}
}
break;
case EVENT_TIMEOUT:
// Your watch face will receive this event after a period of inactivity. If it makes sense to resign,
// you may uncomment this line to move back to the first watch face in the list:
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_sleep_animation(500);
break;
case EVENT_LIGHT_BUTTON_DOWN:
// don't light up every time light is hit
break;
default:
movement_default_loop_handler(event);
break;
}
// return true if the watch can enter standby mode. If you are PWM'ing an LED or buzzing the buzzer here,
// you should return false since the PWM driver does not operate in standby mode.
return true;
}
void tachymeter_face_resign(void *context) {
(void)context;
// handle any cleanup before your watch face goes off-screen.
}

View File

@@ -0,0 +1,109 @@
/*
* MIT License
*
* Copyright (c) 2022 Raymundo Cassani
*
* 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 TACHYMETER_FACE_H_
#define TACHYMETER_FACE_H_
/*
* TACHYMETER face
*
* The Tachymeter complication emulates the tachymeter function often
* present in watches, that computes the average speed in [units per hour]
* for a given distance given in [units].
*
* Use case:
* User sets the distance
* User starts the tachymeter when the trip begins
* User stops the tachymeter when the trip ends
* The watch presents the average speed and trip duration in seconds
*
* Usage:
* Go to tachymeter face, TC is shown in the Weekday Digits
* A steady d in the Day Digits indicates the distance to be used.
* To edit the distance:
* Long-press the Alarm button, the distance edition page (d will blink)
* Use the Light button to change the editing (blinking) digit, and press Alarm to increase its value
* Once done, long-press the Alarm button to exit the distance edition page
* Press the Alarm button to start the tachymeter.
* A running animation will appear in the Day Digits
* Press the Alarm button to stop the tachymeter
* The average speed and total time information will alternate.
* The average speed will be shown alongside /h in the Day Digits;
* and the total time will be shown alongside t in the Day Digits.
* Long press the Light button to return to the distance d page,
* and restart the tachymeter from there.
* Long-press the light button in the steady distance page to reset
* the distance to 1.00
*
* Pending design points
* o movement_request_tick_frequency(4) is used to obtain a 4Hz ticking, thus
* having a time resolution of 250 ms. Not sure if using event.subsecond`
* is the proper way to get the fractions of second for the start and
* final times.
* o For distance and average speed, the Second Digits (position 8 and 9)
* can be seen as decimals, thus possible to show distances as short as
* 0.01 km (or miles) and speeds as low as 0.01 km/h (or mph). However,
* if the same idea is used for the total time (showing hundredths),
* this limits the display to 9999.99 seconds (~2h:45m).
*/
#include "movement.h"
typedef struct {
uint8_t thousands: 4; // 0-9 (must wrap at 10)
uint8_t hundreds: 4; // 0-9 (must wrap at 10)
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)
} distance_digits_t;
typedef struct {
bool running; // tachymeter status
bool editing; // editing distance
uint8_t active_digit; // active digit at editing distance
uint8_t animation_state; // running animation state
watch_date_time_t start_seconds; // start_seconds
int8_t start_subsecond; // start_subsecond count (each count = 250 ms)
distance_digits_t dist_digits; // distance digitwise
uint32_t distance; // distance
uint32_t total_time; // total_time = now - start_time (in cs)
uint32_t total_speed; // 3600 * 100 * distance / total_time
} tachymeter_state_t;
void tachymeter_face_setup(uint8_t watch_face_index, void ** context_ptr);
void tachymeter_face_activate(void *context);
bool tachymeter_face_loop(movement_event_t event, void *context);
void tachymeter_face_resign(void *context);
#define tachymeter_face ((const watch_face_t){ \
tachymeter_face_setup, \
tachymeter_face_activate, \
tachymeter_face_loop, \
tachymeter_face_resign, \
NULL, \
})
#endif // TACHYMETER_FACE_H_

View File

@@ -0,0 +1,212 @@
/*
* MIT License
*
* Copyright (c) 2022 Andrew Mike
*
* 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 "tally_face.h"
#include "watch.h"
#define TALLY_FACE_MAX 9999
#define TALLY_FACE_MIN -99
static bool _init_val;
static bool _quick_ticks_running;
static const int16_t _tally_default[] = {
0,
#ifdef TALLY_FACE_PRESETS_MTG
20,
40,
#endif /* TALLY_FACE_PRESETS_MTG */
#ifdef TALLY_FACE_PRESETS_YUGIOH
4000,
8000,
#endif /* TALLY_FACE_PRESETS_YUGIOH */
};
#define TALLY_FACE_PRESETS_SIZE() (sizeof(_tally_default) / sizeof(int16_t))
void tally_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(tally_state_t));
memset(*context_ptr, 0, sizeof(tally_state_t));
tally_state_t *state = (tally_state_t *)*context_ptr;
state->tally_default_idx = 0;
state->tally_idx = _tally_default[state->tally_default_idx];
_init_val = true;
}
}
void tally_face_activate(void *context) {
(void) context;
_quick_ticks_running = false;
}
static void start_quick_cyc(void){
_quick_ticks_running = true;
movement_request_tick_frequency(8);
}
static void stop_quick_cyc(void){
_quick_ticks_running = false;
movement_request_tick_frequency(1);
}
static void tally_face_increment(tally_state_t *state, bool sound_on) {
bool soundOn = !_quick_ticks_running && sound_on;
_init_val = false;
if (state->tally_idx >= TALLY_FACE_MAX){
if (soundOn) watch_buzzer_play_note(BUZZER_NOTE_E7, 30);
}
else {
state->tally_idx++;
print_tally(state, sound_on);
if (soundOn) watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
}
}
static void tally_face_decrement(tally_state_t *state, bool sound_on) {
bool soundOn = !_quick_ticks_running && sound_on;
_init_val = false;
if (state->tally_idx <= TALLY_FACE_MIN){
if (soundOn) watch_buzzer_play_note(BUZZER_NOTE_C5SHARP_D5FLAT, 30);
}
else {
state->tally_idx--;
print_tally(state, sound_on);
if (soundOn) watch_buzzer_play_note(BUZZER_NOTE_C6SHARP_D6FLAT, 30);
}
}
static bool tally_face_should_move_back(tally_state_t *state) {
if (TALLY_FACE_PRESETS_SIZE() <= 1) { return false; }
return state->tally_idx == _tally_default[state->tally_default_idx];
}
bool tally_face_loop(movement_event_t event, void *context) {
tally_state_t *state = (tally_state_t *)context;
static bool using_led = false;
if (using_led) {
if(!HAL_GPIO_BTN_MODE_read() && !HAL_GPIO_BTN_LIGHT_read() && !HAL_GPIO_BTN_ALARM_read())
using_led = false;
else {
if (event.event_type == EVENT_LIGHT_BUTTON_DOWN || event.event_type == EVENT_ALARM_BUTTON_DOWN)
movement_illuminate_led();
return true;
}
}
switch (event.event_type) {
case EVENT_TICK:
if (_quick_ticks_running) {
bool light_pressed = HAL_GPIO_BTN_LIGHT_read();
bool alarm_pressed = HAL_GPIO_BTN_ALARM_read();
if (light_pressed && alarm_pressed) stop_quick_cyc();
else if (light_pressed) tally_face_increment(state, movement_button_should_sound());
else if (alarm_pressed) tally_face_decrement(state, movement_button_should_sound());
else stop_quick_cyc();
}
break;
case EVENT_ALARM_BUTTON_UP:
tally_face_decrement(state, movement_button_should_sound());
break;
case EVENT_ALARM_LONG_PRESS:
tally_face_decrement(state, movement_button_should_sound());
start_quick_cyc();
break;
case EVENT_MODE_LONG_PRESS:
if (tally_face_should_move_back(state)) {
_init_val = true;
movement_move_to_face(0);
}
else {
state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index
_init_val = true;
//play a reset tune
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30);
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30);
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
print_tally(state, movement_button_should_sound());
}
break;
case EVENT_LIGHT_BUTTON_UP:
tally_face_increment(state, movement_button_should_sound());
break;
case EVENT_LIGHT_BUTTON_DOWN:
case EVENT_ALARM_BUTTON_DOWN:
if (HAL_GPIO_BTN_MODE_read()) {
movement_illuminate_led();
using_led = true;
}
break;
case EVENT_LIGHT_LONG_PRESS:
if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){
state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE();
state->tally_idx = _tally_default[state->tally_default_idx];
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30);
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30);
print_tally(state, movement_button_should_sound());
}
else{
tally_face_increment(state, movement_button_should_sound());
start_quick_cyc();
}
break;
case EVENT_ACTIVATE:
print_tally(state, movement_button_should_sound());
break;
case EVENT_TIMEOUT:
// ignore timeout
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
// print tally index at the center of display.
void print_tally(tally_state_t *state, bool sound_on) {
char buf[14];
if (sound_on)
watch_set_indicator(WATCH_INDICATOR_BELL);
else
watch_clear_indicator(WATCH_INDICATOR_BELL);
if (state->tally_idx >= 0)
sprintf(buf, "TA %4d ", (int)(state->tally_idx)); // center of LCD display
else
sprintf(buf, "TA %-3d", (int)(state->tally_idx)); // center of LCD display
watch_display_string(buf, 0);
}
void tally_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,76 @@
/*
* MIT License
*
* Copyright (c) 2022 Andrew Mike
*
* 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 TALLY_FACE_H_
#define TALLY_FACE_H_
/*
* TALLY face
*
* Tally face is designed to act as a tally counter.
*
* Alarm
* Press: Decrement
* Hold : Fast Decrement
*
* Light
* Press: Increment
* Hold : On initial value: Cycles through other initial values.
* Else: Fast Increment
*
* Mode
* Press: Next face
* Hold : On initial value: Go to first face.
* Else: Resets counter
*
* Incrementing or Decrementing the tally will beep if Beeping is set in the global Preferences
*/
#include "movement.h"
typedef struct {
int16_t tally_idx;
uint8_t tally_default_idx;
} tally_state_t;
//#define TALLY_FACE_PRESETS_MTG
//#define TALLY_FACE_PRESETS_YUGIOH
void tally_face_setup(uint8_t watch_face_index, void ** context_ptr);
void tally_face_activate(void *context);
bool tally_face_loop(movement_event_t event, void *context);
void tally_face_resign(void *context);
void print_tally(tally_state_t *state, bool sound_on);
#define tally_face ((const watch_face_t){ \
tally_face_setup, \
tally_face_activate, \
tally_face_loop, \
tally_face_resign, \
NULL, \
})
#endif // TALLY_FACE_H_

View File

@@ -0,0 +1,297 @@
/*
* MIT License
*
* Copyright (c) 2022 Jeremy O'Brien
* Base code copied from Spencer Bywater's probability face
*
* 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 "tarot_face.h"
#define TAROT_ANIMATION_TICK_FREQUENCY 8
#define FLIPPED_BIT_POS 7
#define FLIPPED_MASK ((uint8_t)(1 << FLIPPED_BIT_POS))
// --------------
// Custom methods
// --------------
static char major_arcana[][7] = {
" FOOL ",
"Mgcian",
"HPrsts",
"En&prs", // Empress
"En&por", // Emperor
"Hiroph",
"Lovers",
"Chriot",
"Strgth",
"Hrn&it", // Hermit
" Frtun",
"Justce",
"Hangn&", // Hangman
" Death",
" tmprn",
" Devil",
" Tower",
" Star",
"n&OON ", // Moon
" Sun ",
"Jdgmnt",
" World",
};
#define NUM_MAJOR_ARCANA (sizeof(major_arcana) / sizeof(*major_arcana))
static char suits[][7] = {
" wands",
" cups",
"swords",
" coins",
};
#define NUM_MINOR_ARCANA 56
#define NUM_CARDS_PER_SUIT 14
#define NUM_TAROT_CARDS (NUM_MAJOR_ARCANA + NUM_MINOR_ARCANA)
static void init_deck(tarot_state_t *state) {
memset(state->drawn_cards, 0xff, sizeof(state->drawn_cards));
state->current_card = 0;
}
static void tarot_display(tarot_state_t *state) {
char buf[12];
char *start_end_string;
uint8_t card;
bool flipped;
// deck is initialized; show current draw mode and return
if (state->drawn_cards[0] == 0xff) {
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
if (state->major_arcana_only) {
sprintf(buf, "TA%2dn&ajor", state->num_cards_to_draw);
} else {
sprintf(buf, "TA%2d All", state->num_cards_to_draw);
}
watch_display_string(buf, 0);
return;
}
// show a special status if we're looking at the first or last card in the spread
if (state->current_card == 0) {
start_end_string = "St";
} else if (state->current_card == state->num_cards_to_draw - 1) {
start_end_string = "En";
} else {
start_end_string = " ";
}
// figure out the card we're showing
card = state->drawn_cards[state->current_card];
flipped = (card & FLIPPED_MASK) ? true : false; // check flipped bit
card &= ~FLIPPED_MASK; // remove the flipped bit
if (card < NUM_MAJOR_ARCANA) {
// major arcana
// show start/end, no rank, card name
sprintf(buf, "%s %s", start_end_string, major_arcana[card]);
} else {
// minor arcana
uint8_t suit = (card - NUM_MAJOR_ARCANA) / NUM_CARDS_PER_SUIT;
uint8_t rank = ((card - NUM_MAJOR_ARCANA) % NUM_CARDS_PER_SUIT) + 1;
// show start/end, rank + suit
sprintf(buf, "%s%2d%s", start_end_string, rank, suits[suit]);
}
watch_display_string(buf, 0);
if (flipped) {
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
} else {
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
}
}
static uint8_t get_rand_num(uint8_t num_values) {
// Emulator: use rand. Hardware: use arc4random.
#if __EMSCRIPTEN__
return rand() % num_values;
#else
return arc4random_uniform(num_values);
#endif
}
static uint8_t draw_one_card(tarot_state_t *state) {
if (state->major_arcana_only) {
return get_rand_num(NUM_MAJOR_ARCANA);
} else {
return get_rand_num(NUM_TAROT_CARDS);
}
}
static bool already_drawn(tarot_state_t *state, uint8_t drawn_card) {
for (int i = 0; state->drawn_cards[i] != 0xff && i < state->num_cards_to_draw; i++) {
if ((state->drawn_cards[i] & ~FLIPPED_MASK) == drawn_card) {
return true;
}
}
return false;
}
static void pick_cards(tarot_state_t *state) {
uint8_t card;
for (int i = 0; i < state->num_cards_to_draw; i++) {
card = draw_one_card(state);
while (already_drawn(state, card)) {
card = draw_one_card(state);
}
card |= get_rand_num(2) << FLIPPED_BIT_POS; // randomly flip the card
state->drawn_cards[i] = card;
}
}
static void display_animation(tarot_state_t *state) {
if (state->animation_frame == 0) {
watch_display_string(" ", 7);
watch_set_pixel(1, 4);
watch_set_pixel(1, 6);
state->animation_frame = 1;
} else if (state->animation_frame == 1) {
watch_clear_pixel(1, 4);
watch_clear_pixel(1, 6);
watch_set_pixel(2, 4);
watch_set_pixel(0, 6);
state->animation_frame = 2;
} else if (state->animation_frame == 2) {
watch_clear_pixel(2, 4);
watch_clear_pixel(0, 6);
watch_set_pixel(2, 5);
watch_set_pixel(0, 5);
state->animation_frame = 3;
} else if (state->animation_frame == 3) {
state->animation_frame = 0;
state->is_picking = false;
movement_request_tick_frequency(1);
tarot_display(state);
}
}
// ---------------------------
// Standard watch face methods
// ---------------------------
void tarot_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(tarot_state_t));
memset(*context_ptr, 0, sizeof(tarot_state_t));
}
// Emulator only: Seed random number generator
#if __EMSCRIPTEN__
srand(time(NULL));
#endif
}
void tarot_face_activate(void *context) {
tarot_state_t *state = (tarot_state_t *)context;
watch_display_string("TA", 0);
init_deck(state);
state->num_cards_to_draw = 3;
state->major_arcana_only = true;
}
bool tarot_face_loop(movement_event_t event, void *context) {
tarot_state_t *state = (tarot_state_t *)context;
if (state->is_picking && event.event_type != EVENT_TICK) {
return true;
}
switch (event.event_type) {
case EVENT_ACTIVATE:
tarot_display(state);
break;
case EVENT_TICK:
if (state->is_picking) {
display_animation(state);
}
break;
case EVENT_LIGHT_BUTTON_UP:
if (state->drawn_cards[0] == 0xff) {
// deck is inited; cycle through # cards to draw
state->num_cards_to_draw++;
if (state->num_cards_to_draw > 10) {
state->num_cards_to_draw = 3;
}
} else {
// cycle through the drawn cards
state->current_card = (state->current_card + 1) % state->num_cards_to_draw;
}
tarot_display(state);
break;
case EVENT_LIGHT_LONG_PRESS:
if (state->drawn_cards[0] == 0xff) {
// at main screen; cycle major arcana mode
state->major_arcana_only = !state->major_arcana_only;
} else {
// at card view screen; go back to draw screen
init_deck(state);
}
tarot_display(state);
break;
case EVENT_ALARM_BUTTON_UP:
// Draw cards
watch_display_string(" ", 4);
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
init_deck(state);
pick_cards(state);
state->is_picking = true;
// card picking animation begins on next tick and new cards will be displayed on completion
movement_request_tick_frequency(TAROT_ANIMATION_TICK_FREQUENCY);
break;
case EVENT_LOW_ENERGY_UPDATE:
watch_display_string("SLEEP ", 4);
break;
case EVENT_LIGHT_BUTTON_DOWN:
// don't light up every time light is hit
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void tarot_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,89 @@
/*
* MIT License
*
* Copyright (c) 2022 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 TAROT_FACE_H_
#define TAROT_FACE_H_
/*
* TAROT CARD watch face
*
* Draw from a deck of tarot cards. Can choose between major arcana only or
* entire deck.
*
* In tarot reading, a card orientation can be upright or inverted, and the
* interpertation of the card can change depending on this state. This face
* lights the alarm indicator to show when a card is inverted. Just ignore it
* if you prefer not to deal with card inversions.
*
* This face uses the terms "Wands", "Cups", "Swords" and "Coins" for the four
* suits, and numbers to represent the 14 ranked cards, with the cards 11-14
* representing the Page, the Knight, the Queen, and King respectively.
*
* Default draw is a 3-card major arcana spread.
*
* To make it easier to keep track of where you are in the list of drawn cards,
* after drawing, "St" is shown for the 1st card in the spread and "En" is
* shown for the last card.
*
* At any point, the mode button can be held to return to your first configured
* watch face.
*
* When "Major" or "All" is shown:
* - Light button: cycle # of cards to draw
* - Light button (long press): toggle between major arcana and all cards
* - Alarm button: shuffle deck and draw cards
*
* After cards are drawn/showing:
* - Light button: view the next drawn card
* - Alarm button: shuffle and re-draw new cards
* - Light button (long press): go back to Draw screen, for choosing different draw parameters.
*/
#include "movement.h"
#define MAX_CARDS_TO_DRAW 10
typedef struct {
uint8_t drawn_cards[MAX_CARDS_TO_DRAW];
uint8_t current_card;
uint8_t animation_frame;
uint8_t num_cards_to_draw;
bool major_arcana_only;
bool is_picking;
} tarot_state_t;
void tarot_face_setup(uint8_t watch_face_index, void ** context_ptr);
void tarot_face_activate(void *context);
bool tarot_face_loop(movement_event_t event, void *context);
void tarot_face_resign(void *context);
#define tarot_face ((const watch_face_t){ \
tarot_face_setup, \
tarot_face_activate, \
tarot_face_loop, \
tarot_face_resign, \
NULL, \
})
#endif // TAROT_FACE_H_

View File

@@ -0,0 +1,145 @@
/*
* MIT License
*
* Copyright (c) 2022 Mikhail Svarichevsky
*
* 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 <math.h>
#include "tempchart_face.h"
#include "watch.h"
#include "watch_private_display.h"
#include "filesystem.h"
#include "thermistor_driver.h"
struct {
uint8_t stat[24 * 70];
uint16_t num_div;
} tempchart_state;
static void tempchart_save(void) {
filesystem_write_file("tempchart.ini", (char*)&tempchart_state, sizeof(tempchart_state));
}
void tempchart_face_setup(uint8_t watch_face_index, void ** context_ptr) {
// These next two lines just silence the compiler warnings associated with unused parameters.
// We have no use for the settings or the watch_face_index, so we make that explicit here.
(void) context_ptr;
(void) watch_face_index;
// At boot, context_ptr will be NULL indicating that we don't have anyplace to store our context.
if (filesystem_get_file_size("tempchart.ini") != sizeof(tempchart_state)) {
// No previous ini or old version of ini file - create new config file
tempchart_state.num_div = 0;
for (int i = 0; i < 24 * 70; i++)
tempchart_state.stat[i] = 0;
tempchart_save();
} else
filesystem_read_file("tempchart.ini", (char*)&tempchart_state, sizeof(tempchart_state));
}
void tempchart_face_activate(void *context) {
// same as above: silence the warning, we don't need to check the settings.
(void) context;
}
static void display(void) {
int sum = 0;
for (int i = 0; i < 24 * 70; i++)
sum += tempchart_state.stat[i];
char buf[24];
sprintf(buf, "TS%2d%6d", tempchart_state.num_div, sum);
watch_display_string(buf, 0);
}
bool tempchart_face_loop(movement_event_t event, void *context) {
(void) context;
switch (event.event_type) {
case EVENT_ACTIVATE:
display();
case EVENT_TICK:
// on activate and tick, if we are animating,
break;
case EVENT_LOW_ENERGY_UPDATE:
// This low energy mode update occurs once a minute, if the watch face is in the
// foreground when Movement enters low energy mode. We have the option of supporting
// this mode, but since our watch face animates once a second, the "Hello there" face
// isn't very useful in this mode. So we choose not to support it. (continued below)
break;
case EVENT_TIMEOUT:
// ... Instead, we respond to the timeout event. This event happens after a configurable
// interval on screen (1-30 minutes). The watch will give us this event as a chance to
// resign control if we want to, and in this case, we do.
// This function will return the watch to the first screen (usually a simple clock),
// and it will do it long before the watch enters low energy mode. This ensures we
// won't be on screen, and thus opts us out of getting the EVENT_LOW_ENERGY_UPDATE above.
movement_move_to_face(0);
break;
case EVENT_BACKGROUND_TASK:
// Here we measure temperature and do main frequency correction
thermistor_driver_enable();
float temperature_c = thermistor_driver_get_temperature();
thermistor_driver_disable();
watch_date_time_t date_time = watch_rtc_get_date_time();
int temp = round(temperature_c * 2);
if ((temp < 0) || (temp >= 70)) break;
if (tempchart_state.stat[date_time.unit.hour + temp * 24] == 255) { // We've reached the limit
tempchart_state.num_div++;
for (int i = 0; i < 24 * 70; i++)
tempchart_state.stat[i] = (tempchart_state.stat[i] + 1) >> 1; // So that we don't lose 1
}
tempchart_state.stat[date_time.unit.hour+temp*24]++;
if (date_time.unit.hour == 0 && date_time.unit.minute == 10)
tempchart_save();
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void tempchart_face_resign(void *context) {
// our watch face, like most watch faces, has nothing special to do when resigning.
// watch faces that enable a peripheral or interact with a sensor may want to turn it off here.
(void) context;
}
//background freq correction
movement_watch_face_advisory_t tempchart_face_advise(void *context) {
(void) context;
movement_watch_face_advisory_t retval = { 0 };
watch_date_time_t date_time = watch_rtc_get_date_time();
// Updating data every 5 minutes
retval.wants_background_task = date_time.unit.minute % 5 == 0;
return retval;
}

View 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.
*/
#ifndef TEMPCHART_FACE_H_
#define TEMPCHART_FACE_H_
/*
* TEMPERATURE CHART face
*
* Gathers temperature statistics in a chart form.
* Statistics bins are per hour / per 0.5°C.
*
* Saved to file every day at 00:00.
* Can help improve watch precision in the future.
*
* If you can gather statistics over few months, and then send "tempchart.ini"
* to "3@14.by", it will help future generations of precision quartz watches.
*/
#include "movement.h"
void tempchart_face_setup(uint8_t watch_face_index, void ** context_ptr);
void tempchart_face_activate(void *context);
bool tempchart_face_loop(movement_event_t event, void *context);
void tempchart_face_resign(void *context);
movement_watch_face_advisory_t tempchart_face_advise(void *context);
#define tempchart_face ((const watch_face_t){ \
tempchart_face_setup, \
tempchart_face_activate, \
tempchart_face_loop, \
tempchart_face_resign, \
tempchart_face_advise, \
})
#endif // TEMPCHART_FACE_H_

View File

@@ -0,0 +1,338 @@
/*
* MIT License
*
* Copyright (c) 2022 Andreas Nebinger, based on the work of 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 "time_left_face.h"
#include "watch.h"
#include "watch_private_display.h"
#include "watch_utility.h"
const char _state_titles[][3] = {{'D', 'L', ' '}, {'D', 'L', ' '}, {'D', 'A', ' '}, {'D', 'A', ' '}, {'Y', 'R', 'b'}, {'M', 'O', 'b'}, {'D', 'A', 'b'},
{'Y', 'R', 'd'}, {'M', 'O', 'd'}, {'D', 'A', 'd'}};
const uint8_t TIME_LEFT_FACE_STATES = sizeof(_state_titles) / 3; // total number of state pages
#define TIME_LEFT_FACE_SETTINGS_STATE 4 // number of first settings state
const uint8_t _percentage_segdata[][2] = {{1, 2}, {2, 2}, {2, 3}, {1, 3}}; // segment data for drawing the percentage sign
const uint8_t _animation_segdata[][2] = {{2, 8}, {1, 8}, {2, 7}, {2, 6}}; // segment data for the ticking animation
static bool _quick_ticks_running;
static uint32_t _juliandaynum(uint16_t year, uint16_t month, uint16_t day) {
// from here: https://en.wikipedia.org/wiki/Julian_day#Julian_day_number_calculation
return (1461 * (year + 4800 + (month - 14) / 12)) / 4 + (367 * (month - 2 - 12 * ((month - 14) / 12))) / 12 - (3 * ((year + 4900 + (month - 14) / 12) / 100))/4 + day - 32075;
}
/// @brief displays an integer value with or without using positions 8 and 9
static void _display_integer(char *buf) {
if (buf[1] == ' ') {
// display at position 4 if the value is short enough, don't use positions 8 and 9
watch_display_character(' ', 8);
watch_display_character(' ', 9);
watch_display_string((char *)buf + 2, 4);
} else {
// otherwise just display using position 8 and 9
watch_display_string(buf, 4);
}
watch_clear_colon();
}
///@brief display a percentage value
static void _display_percentage(float percentage, char *buf) {
// always stay positive
if (percentage < 0) {
percentage *= -1;
// Switch display to days 'O'ver
watch_display_character('O', 1);
}
int32_t integral = percentage;
if (integral >= 100) {
// percentage equals 100 or more: don't do fractional part
sprintf(buf, " %3li o", integral);
watch_clear_colon();
} else {
// display percentage with two decimal places
uint8_t fraction = (int)(percentage * (float)100) % 100;
sprintf(buf, "%2li%02u o", integral, fraction);
watch_set_colon();
}
watch_display_string(buf, 4);
// draw (parts of) percentage symbol
for (uint8_t i = 0; i < sizeof(_percentage_segdata) / 2; i++)
watch_set_pixel(_percentage_segdata[i][0], _percentage_segdata[i][1]);
}
/// @brief draw the current state to the display
static void _draw(time_left_state_t *state, uint8_t subsecond) {
char buf[17];
watch_display_character(_state_titles[state->current_page][0], 0);
watch_display_character(_state_titles[state->current_page][1], 1);
watch_display_character(' ', 2);
watch_display_character(_state_titles[state->current_page][2], 3);
if (state->current_page < TIME_LEFT_FACE_SETTINGS_STATE) {
// we are displaying days left or days from birth
watch_date_time_t date_time = watch_rtc_get_date_time();
uint32_t julian_current_day = _juliandaynum(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR, date_time.unit.month, date_time.unit.day);
uint32_t julian_target_day = _juliandaynum(state->target_date.bit.year, state->target_date.bit.month, state->target_date.bit.day);
int32_t days_left = julian_target_day - julian_current_day;
if (state->current_page == 0) {
// display number of days left
sprintf(buf, "%6li", days_left);
_display_integer(buf);
} else {
// calculate starting date
uint32_t julian_start_day = _juliandaynum(state->birth_date.bit.year, state->birth_date.bit.month, state->birth_date.bit.day);
if ((state->current_page & 1) == 1) {
float percentage_left;
if (julian_start_day == julian_target_day) {
// failsafe
percentage_left = 0;
} else {
// display correct percentages
percentage_left = (float)days_left * (float)100 / (float)(julian_target_day - julian_start_day);
}
_display_percentage(state->current_page == 1 ? percentage_left : 100 - percentage_left, buf);
} else {
// display days from birth
sprintf(buf, "%6li", (int32_t)julian_current_day - julian_start_day);
_display_integer(buf);
}
}
} else {
// we are in settings mode
switch (state->current_page) {
case TIME_LEFT_FACE_SETTINGS_STATE:
// birth year
sprintf(buf, "%04u ", state->birth_date.bit.year);
break;
case TIME_LEFT_FACE_SETTINGS_STATE + 1:
// birth month
sprintf(buf, " %02u", state->birth_date.bit.month);
break;
case TIME_LEFT_FACE_SETTINGS_STATE + 2:
// birth day of month
sprintf(buf, " %02u", state->birth_date.bit.day);
break;
case TIME_LEFT_FACE_SETTINGS_STATE + 3:
// target year
sprintf(buf, "%04u", state->target_date.bit.year);
break;
case TIME_LEFT_FACE_SETTINGS_STATE + 4:
// target month
sprintf(buf, " %02u", state->target_date.bit.month);
break;
case TIME_LEFT_FACE_SETTINGS_STATE + 5:
// target day of month
sprintf(buf, " %02u", state->target_date.bit.day);
break;
default:
break;
}
// blink current settings position or display value
if ((subsecond & 1) == 1)
watch_display_string(" ", 4);
else
watch_display_string(buf, 4);
}
}
/// @brief handle short or long pressing the alarm button
static void _handle_alarm_button(time_left_state_t *state) {
switch (state->current_page) {
case TIME_LEFT_FACE_SETTINGS_STATE: // birth year
state->birth_date.bit.year++;
if (state->birth_date.bit.year > state->current_year + 10) state->birth_date.bit.year = 1959;
break;
case TIME_LEFT_FACE_SETTINGS_STATE + 1: // birth month
state->birth_date.bit.month = (state->birth_date.bit.month % 12) + 1;
break;
case TIME_LEFT_FACE_SETTINGS_STATE + 2: // birth day
state->birth_date.bit.day = (state->birth_date.bit.day % watch_utility_days_in_month(state->birth_date.bit.month, state->birth_date.bit.year)) + 1;
break;
case TIME_LEFT_FACE_SETTINGS_STATE + 3: // target year
state->target_date.bit.year++;
if (state->target_date.bit.year >2083) state->target_date.bit.year = state->current_year - 10;
break;
case TIME_LEFT_FACE_SETTINGS_STATE + 4: // target month
state->target_date.bit.month = (state->target_date.bit.month % 12) + 1;
break;
case TIME_LEFT_FACE_SETTINGS_STATE + 5: // target day
state->target_date.bit.day = (state->target_date.bit.day % watch_utility_days_in_month(state->target_date.bit.month, state->birth_date.bit.year)) + 1;
break;
}
}
static void _initiate_setting(time_left_state_t *state) {
state->current_page = TIME_LEFT_FACE_SETTINGS_STATE;
watch_clear_colon();
movement_request_tick_frequency(4);
}
static void _resume_setting(time_left_state_t *state) {
state->current_page = 0;
movement_request_tick_frequency(1);
}
static void _abort_quick_ticks() {
if (_quick_ticks_running) {
_quick_ticks_running = false;
movement_request_tick_frequency(4);
}
}
void time_left_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(time_left_state_t));
memset(*context_ptr, 0, sizeof(time_left_state_t));
time_left_state_t *state = (time_left_state_t *)*context_ptr;
state->birth_date.reg = watch_get_backup_data(2);
if (state->birth_date.reg == 0) {
// if birth date is totally blank, set a reasonable starting date. this works well for anyone under 63, but
// you can keep pressing to go back to 1900; just pass the current year. also picked this date because if you
// set it to 1959-01-02, it counts up from the launch of Luna-1, the first spacecraft to leave the well.
state->birth_date.bit.year = 1959;
state->birth_date.bit.month = 1;
state->birth_date.bit.day = 1;
watch_store_backup_data(state->birth_date.reg, 2);
// set target date to today + 10 years (just to have any value)
watch_date_time_t date_time = watch_rtc_get_date_time();
state->target_date.bit.year = date_time.unit.year + WATCH_RTC_REFERENCE_YEAR + 10;
state->target_date.bit.month = date_time.unit.month;
state->target_date.bit.day = date_time.unit.day;
}
}
}
void time_left_face_activate(void *context) {
time_left_state_t *state = (time_left_state_t *)context;
// stash the current year, useful in birthday setting mode
watch_date_time_t date_time = watch_rtc_get_date_time();
state->current_year = date_time.unit.year + WATCH_RTC_REFERENCE_YEAR;
_quick_ticks_running = false;
// fetch the user's birth date from the birthday register
state->birth_date.reg = watch_get_backup_data(2);
// store original value of the birthdate to be able to detect changes on resigning
state->birth_date_when_activated.reg = state->birth_date.reg;
}
bool time_left_face_loop(movement_event_t event, void *context) {
time_left_state_t *state = (time_left_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
_draw(state, event.subsecond);
break;
case EVENT_LOW_ENERGY_UPDATE:
case EVENT_TICK: {
uint8_t subsecond = event.subsecond;
if (_quick_ticks_running) {
if (HAL_GPIO_BTN_ALARM_read()) {
_handle_alarm_button(state);
subsecond = 0;
} else _abort_quick_ticks();
}
if (state->current_page >= TIME_LEFT_FACE_SETTINGS_STATE) {
// we are in settings mode. Draw to blink
_draw(state, subsecond);
} else {
// otherwise, check if we have to update. the display only needs to change at midnight
watch_date_time_t date_time = watch_rtc_get_date_time();
if (date_time.unit.hour == 0 && date_time.unit.minute == 0 && date_time.unit.second == 0) {
_draw(state, subsecond);
}
// handle the ticking animation
uint8_t animation_step = date_time.unit.second % (sizeof(_animation_segdata) / 2);
watch_set_pixel(_animation_segdata[animation_step][0], _animation_segdata[animation_step][1]);
if (animation_step == 0) animation_step = (sizeof(_animation_segdata) / 2) - 1;
else animation_step--;
watch_clear_pixel(_animation_segdata[animation_step][0], _animation_segdata[animation_step][1]);
}
break;
}
case EVENT_LIGHT_BUTTON_DOWN:
// do not illuminate here (this is done when releasing the button)
break;
case EVENT_LIGHT_BUTTON_UP:
if (state->current_page < TIME_LEFT_FACE_SETTINGS_STATE) movement_illuminate_led();
else {
// cycle through the settings pages
state->current_page++;
if (state->current_page >= TIME_LEFT_FACE_STATES) {
// we have done a full settings cycle, so resume to normal
_resume_setting(state);
_draw(state, event.subsecond);
}
}
break;
case EVENT_LIGHT_LONG_PRESS:
if (state->current_page >= TIME_LEFT_FACE_SETTINGS_STATE) {
_resume_setting(state);
} else {
_initiate_setting(state);
}
_draw(state, event.subsecond);
break;
case EVENT_ALARM_BUTTON_UP:
_abort_quick_ticks();
if (state->current_page < TIME_LEFT_FACE_SETTINGS_STATE) {
// alternate between days left and percentage left
state->current_page = (state->current_page + 1) % TIME_LEFT_FACE_SETTINGS_STATE;
} else {
// we are in settings mode, so increment the current settings value
_handle_alarm_button(state);
}
_draw(state, 0);
break;
case EVENT_ALARM_LONG_PRESS:
if (state->current_page >= TIME_LEFT_FACE_SETTINGS_STATE) {
// initiate fast cycling for settings values
movement_request_tick_frequency(8);
_quick_ticks_running = true;
_handle_alarm_button(state);
_draw(state, 0);
}
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void time_left_face_resign(void *context) {
time_left_state_t *state = (time_left_state_t *)context;
if (state->current_page >= TIME_LEFT_FACE_SETTINGS_STATE) _resume_setting(state);
// if the user changed their birth date, store it to the birth date register
if (state->birth_date_when_activated.reg != state->birth_date.reg) {
watch_store_backup_data(state->birth_date.reg, 2);
}
}

View File

@@ -0,0 +1,91 @@
/*
* MIT License
*
* Copyright (c) 2022 Andreas Nebinger, based on the work of 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 TIME_LEFT_FACE_H_
#define TIME_LEFT_FACE_H_
/*
* TIME LEFT face
*
* The Time Left Face helps you to visualize how far you have proceeded in a certain
* time span. Much like the Day One Face, you can set your beginning date. In addition
* to that, you also set your target or destination date. You can then use the face
* to display your progress in different ways.
*
* Usage:
*
* - Long pressing of the light button starts the settings mode:
* - First, you set the beginning date (indicated by a 'b' in the upper right corner).
* - Start by setting the year (indicated by the letter 'YR'). Use the alarm button
* to cycle the value. Short pressing the light button brings you to the next
* settings page.
* - Set the values in this order:
* a. beginning date (indicated by a 'b'): year - month - day
* b. destination date (indicated by a 'd'): year - month - day
* - After cycling through all settings pages, the face resumes to display mode.
*
* - In display mode, use the alarm button (short press) to cycle through these four
* types of display:
* a. number of days left ('DL') until the destination date is reached.
* b. remaining days expressed as percentage of total time span. The value is shown
* with two decimals, using the colon as decimal point.
* c. number of days passed ('DA') since the beginning date.
* d. number of days passed expressed as percentage of total time span. (Two decimal
* points.)
*
* What is this for?
*
* You can use this watch face to be reminded of any kind of progess between a set
* start and end date. The brave among us can use it as a kind of memento mori
* visualization. Set your date of birth and look up the average life expectancy of
* your age cohort based on publicly available mortality tables. Then, set the
* statistically expected day of death as the target date and you will be able to
* see how much of your time has passed and how much is still to come.
*
*/
#include "movement.h"
typedef struct {
uint8_t current_page;
uint16_t current_year;
movement_birthdate_t birth_date;
movement_birthdate_t birth_date_when_activated;
movement_birthdate_t target_date;
} time_left_state_t;
void time_left_face_setup(uint8_t watch_face_index, void ** context_ptr);
void time_left_face_activate(void *context);
bool time_left_face_loop(movement_event_t event, void *context);
void time_left_face_resign(void *context);
#define time_left_face ((const watch_face_t){ \
time_left_face_setup, \
time_left_face_activate, \
time_left_face_loop, \
time_left_face_resign, \
NULL, \
})
#endif // TIME_LEFT_FACE_H_

View File

@@ -0,0 +1,356 @@
/*
* MIT License
*
* Copyright (c) 2022 Andreas Nebinger, building on Wesley Ellis countdown_face.c
*
* 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 "timer_face.h"
#include "watch.h"
#include "watch_utility.h"
static const uint32_t _default_timer_values[] = {0x000200, 0x000500, 0x000A00, 0x001400, 0x002D02}; // default timers: 2 min, 5 min, 10 min, 20 min, 2 h 45 min
// sound sequence for a single beeping sequence
static const int8_t _sound_seq_beep[] = {BUZZER_NOTE_C8, 3, BUZZER_NOTE_REST, 3, -2, 2, BUZZER_NOTE_C8, 5, BUZZER_NOTE_REST, 25, 0};
static const int8_t _sound_seq_start[] = {BUZZER_NOTE_C8, 2, 0};
static uint8_t _beeps_to_play; // temporary counter for ring signals playing
static void _signal_callback() {
if (_beeps_to_play) {
_beeps_to_play--;
watch_buzzer_play_sequence((int8_t *)_sound_seq_beep, _signal_callback);
}
}
static void _start(timer_state_t *state, bool with_beep) {
if (state->timers[state->current_timer].value == 0) return;
watch_date_time_t now = watch_rtc_get_date_time();
state->now_ts = watch_utility_date_time_to_unix_time(now, movement_get_current_timezone_offset());
if (state->mode == pausing)
state->target_ts = state->now_ts + state->paused_left;
else
state->target_ts = watch_utility_offset_timestamp(state->now_ts,
state->timers[state->current_timer].unit.hours,
state->timers[state->current_timer].unit.minutes,
state->timers[state->current_timer].unit.seconds);
watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(state->target_ts, movement_get_current_timezone_offset());
state->mode = running;
movement_schedule_background_task_for_face(state->watch_face_index, target_dt);
watch_set_indicator(WATCH_INDICATOR_BELL);
if (with_beep) watch_buzzer_play_sequence((int8_t *)_sound_seq_start, NULL);
}
static void _draw(timer_state_t *state, uint8_t subsecond) {
char buf[14];
uint32_t delta;
div_t result;
uint8_t h, min, sec;
switch (state->mode) {
case pausing:
if (state->pausing_seconds % 2)
watch_clear_indicator(WATCH_INDICATOR_BELL);
else
watch_set_indicator(WATCH_INDICATOR_BELL);
if (state->pausing_seconds != 1)
// not 1st iteration (or 256th): do not write anything
return;
// fall through
case running:
delta = state->target_ts - state->now_ts;
result = div(delta, 60);
sec = result.rem;
result = div(result.quot, 60);
min = result.rem;
h = result.quot;
sprintf(buf, " %02u%02u%02u", h, min, sec);
break;
case setting:
if (state->settings_state == 1) {
// ask it the current timer shall be erased
sprintf(buf, " CLEAR%c", state->erase_timer_flag ? 'y' : 'n');
watch_clear_colon();
} else if (state->settings_state == 5) {
sprintf(buf, " LOOP%c", state->timers[state->current_timer].unit.repeat ? 'y' : 'n');
watch_clear_colon();
} else {
sprintf(buf, " %02u%02u%02u", state->timers[state->current_timer].unit.hours,
state->timers[state->current_timer].unit.minutes,
state->timers[state->current_timer].unit.seconds);
watch_set_colon();
}
break;
case waiting:
sprintf(buf, " %02u%02u%02u", state->timers[state->current_timer].unit.hours,
state->timers[state->current_timer].unit.minutes,
state->timers[state->current_timer].unit.seconds);
break;
}
buf[0] = 49 + state->current_timer;
if (state->mode == setting && subsecond % 2) {
// blink the current settings value
if (state->settings_state == 0) buf[0] = ' ';
else if (state->settings_state == 1 || state->settings_state == 5) buf[6] = ' ';
else buf[(state->settings_state - 1) * 2 - 1] = buf[(state->settings_state - 1) * 2] = ' ';
}
watch_display_string(buf, 3);
// set lap indicator when we have a looping timer
if (state->timers[state->current_timer].unit.repeat) watch_set_indicator(WATCH_INDICATOR_LAP);
else watch_clear_indicator(WATCH_INDICATOR_LAP);
}
static void _reset(timer_state_t *state) {
state->mode = waiting;
movement_cancel_background_task_for_face(state->watch_face_index);
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
static void _set_next_valid_timer(timer_state_t *state) {
if ((state->timers[state->current_timer].value & 0xFFFFFF) == 0) {
uint8_t i = state->current_timer;
do {
i = (i + 1) % TIMER_SLOTS;
} while ((state->timers[i].value & 0xFFFFFF) == 0 && i != state->current_timer);
state->current_timer = i;
}
}
static void _resume_setting(timer_state_t *state) {
state->settings_state = 0;
state->mode = waiting;
movement_request_tick_frequency(1);
_set_next_valid_timer(state);
}
static void _settings_increment(timer_state_t *state) {
switch(state->settings_state) {
case 0:
state->current_timer = (state->current_timer + 1) % TIMER_SLOTS;
break;
case 1:
state->erase_timer_flag ^= 1;
break;
case 2:
state->timers[state->current_timer].unit.hours = (state->timers[state->current_timer].unit.hours + 1) % 24;
break;
case 3:
state->timers[state->current_timer].unit.minutes = (state->timers[state->current_timer].unit.minutes + 1) % 60;
break;
case 4:
state->timers[state->current_timer].unit.seconds = (state->timers[state->current_timer].unit.seconds + 1) % 60;
break;
case 5:
state->timers[state->current_timer].unit.repeat ^= 1;
break;
default:
// should never happen
break;
}
return;
}
static void _abort_quick_cycle(timer_state_t *state) {
if (state->quick_cycle) {
state->quick_cycle = false;
movement_request_tick_frequency(4);
}
}
static inline bool _check_for_signal() {
if (_beeps_to_play) {
_beeps_to_play = 0;
return true;
}
return false;
}
void timer_face_setup(uint8_t watch_face_index, void ** context_ptr) {
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(timer_state_t));
timer_state_t *state = (timer_state_t *)*context_ptr;
memset(*context_ptr, 0, sizeof(timer_state_t));
state->watch_face_index = watch_face_index;
for (uint8_t i = 0; i < sizeof(_default_timer_values) / sizeof(uint32_t); i++) {
state->timers[i].value = _default_timer_values[i];
}
}
}
void timer_face_activate(void *context) {
timer_state_t *state = (timer_state_t *)context;
watch_display_string("TR", 0);
watch_set_colon();
if(state->mode == running) {
watch_date_time_t now = watch_rtc_get_date_time();
state->now_ts = watch_utility_date_time_to_unix_time(now, movement_get_current_timezone_offset());
watch_set_indicator(WATCH_INDICATOR_BELL);
} else {
state->pausing_seconds = 1;
_beeps_to_play = 0;
}
}
bool timer_face_loop(movement_event_t event, void *context) {
timer_state_t *state = (timer_state_t *)context;
uint8_t subsecond = event.subsecond;
switch (event.event_type) {
case EVENT_ACTIVATE:
_draw(state, event.subsecond);
break;
case EVENT_TICK:
if (state->mode == running) state->now_ts++;
else if (state->mode == pausing) state->pausing_seconds++;
else if (state->quick_cycle) {
if (HAL_GPIO_BTN_ALARM_read()) {
_settings_increment(state);
subsecond = 0;
} else _abort_quick_cycle(state);
}
_draw(state, subsecond);
break;
case EVENT_LIGHT_BUTTON_DOWN:
switch (state->mode) {
case pausing:
case running:
movement_illuminate_led();
break;
case setting:
if (state->erase_timer_flag) {
state->timers[state->current_timer].value = 0;
state->erase_timer_flag = false;
}
state->settings_state = (state->settings_state + 1) % 6;
if (state->settings_state == 1 && state->timers[state->current_timer].value == 0) state->settings_state = 2;
else if (state->settings_state == 5 && (state->timers[state->current_timer].value & 0xFFFFFF) == 0) state->settings_state = 0;
break;
default:
break;
}
_draw(state, event.subsecond);
break;
case EVENT_LIGHT_BUTTON_UP:
if (state->mode == waiting) movement_illuminate_led();
break;
case EVENT_ALARM_BUTTON_UP:
_abort_quick_cycle(state);
if (_check_for_signal()) break;;
switch (state->mode) {
case running:
state->mode = pausing;
state->pausing_seconds = 0;
state->paused_left = state->target_ts - state->now_ts;
movement_cancel_background_task();
break;
case pausing:
_start(statemovement_get_current_timezone_offset(), false);
break;
case waiting: {
uint8_t last_timer = state->current_timer;
state->current_timer = (state->current_timer + 1) % TIMER_SLOTS;
_set_next_valid_timer(state);
// start the time immediately if there is only one valid timer slot
if (last_timer == state->current_timer) _start(statemovement_get_current_timezone_offset(), true);
break;
}
case setting:
_settings_increment(state);
subsecond = 0;
break;
}
_draw(state, subsecond);
break;
case EVENT_LIGHT_LONG_PRESS:
if (state->mode == waiting) {
// initiate settings
state->mode = setting;
state->settings_state = 0;
state->erase_timer_flag = false;
movement_request_tick_frequency(4);
} else if (state->mode == setting) {
_resume_setting(state);
}
_draw(state, event.subsecond);
break;
case EVENT_BACKGROUND_TASK:
// play the alarm
_beeps_to_play = 4;
watch_buzzer_play_sequence((int8_t *)_sound_seq_beep, _signal_callback);
_reset(state);
if (state->timers[state->current_timer].unit.repeat) _start(statemovement_get_current_timezone_offset(), false);
break;
case EVENT_ALARM_LONG_PRESS:
switch(state->mode) {
case setting:
switch (state->settings_state) {
case 0:
state->current_timer = 0;
break;
case 2:
case 3:
case 4:
state->quick_cycle = true;
movement_request_tick_frequency(8);
break;
default:
break;
}
break;
case waiting:
_start(statemovement_get_current_timezone_offset(), true);
break;
case pausing:
case running:
_reset(state);
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
break;
default:
break;
}
_draw(state, event.subsecond);
break;
case EVENT_ALARM_LONG_UP:
_abort_quick_cycle(state);
break;
case EVENT_MODE_LONG_PRESS:
case EVENT_TIMEOUT:
_abort_quick_cycle(state);
movement_move_to_face(0);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void timer_face_resign(void *context) {
timer_state_t *state = (timer_state_t *)context;
if (state->mode == setting) {
state->settings_state = 0;
state->mode = waiting;
}
}

View File

@@ -0,0 +1,103 @@
/*
* MIT License
*
* Copyright (c) 2022 Andreas Nebinger, based on Wesley Ellis countdown face.
*
* 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 TIMER_FACE_H_
#define TIMER_FACE_H_
/*
* TIMER face
* Advanced timer/countdown face with pre-set timer lengths
*
* This watch face provides the functionality of starting a countdown by choosing
* one out of nine programmable timer presets. A timer/countdown can be 23 hours,
* 59 minutes, and 59 seconds max. A timer can also be set to auto-repeat, which
* is indicated by the lap indicator.
*
* How to use in NORMAL mode:
* - Short-pressing the alarm button cycles through all pre-set timer lengths.
* Find the current timer slot number in the upper right-hand corner.
* - Long-pressing the alarm button starts the timer.
* - Long-pressing the light button initiates settings mode.
*
* How to use in SETTINGS mode:
* - There are up to nine slots for storing a timer setting. The current slot is
* indicated by the number in the upper right-hand corner.
* - Short-pressing the light button cycles through the settings values of each
* timer slot in the following order: hours - minutes - seconds - timer repeat
* - Short-pressing the alarm button alters the current settings value.
* - Long-pressing the light button resumes to normal mode.
*
*/
#include "movement.h"
#define TIMER_SLOTS 9 // offer 9 timer slots
typedef enum {
waiting,
running,
setting,
pausing
} timer_mode_t;
typedef union {
struct {
uint8_t hours;
uint8_t minutes;
uint8_t seconds;
bool repeat;
} unit;
uint32_t value;
} timer_setting_t;
typedef struct {
uint32_t target_ts;
uint32_t now_ts;
uint16_t paused_left;
uint8_t pausing_seconds;
uint8_t watch_face_index;
timer_setting_t timers[TIMER_SLOTS];
uint8_t settings_state : 4;
uint8_t current_timer : 4;
uint8_t set_timers : 4;
bool erase_timer_flag : 1;
timer_mode_t mode : 3;
bool quick_cycle : 1;
} timer_state_t;
void timer_face_setup(uint8_t watch_face_index, void ** context_ptr);
void timer_face_activate(void *context);
bool timer_face_loop(movement_event_t event, void *context);
void timer_face_resign(void *context);
#define timer_face ((const watch_face_t){ \
timer_face_setup, \
timer_face_activate, \
timer_face_loop, \
timer_face_resign, \
NULL, \
})
#endif // TIMER_FACE_H_

View File

@@ -0,0 +1,190 @@
/*
* MIT License
*
* Copyright (c) 2022 Wesley Ellis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFtomato_ringEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include <string.h>
#include "tomato_face.h"
#include "watch_utility.h"
static uint8_t focus_min = 25;
static uint8_t break_min = 5;
static uint8_t get_length(tomato_state_t *state) {
uint8_t length;
if (state->kind == tomato_focus) {
length = focus_min;
} else {
length = break_min;
}
return length;
}
static void tomato_start(tomato_state_t *state) {
watch_date_time_t now = watch_rtc_get_date_time();
int8_t length = (int8_t) get_length(state);
state->mode = tomato_run;
state->now_ts = watch_utility_date_time_to_unix_time(now, movement_get_current_timezone_offset());
state->target_ts = watch_utility_offset_timestamp(state->now_ts, 0, length, 0);
watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(state->target_ts, movement_get_current_timezone_offset());
movement_schedule_background_task(target_dt);
watch_set_indicator(WATCH_INDICATOR_BELL);
}
static void tomato_draw(tomato_state_t *state) {
char buf[16];
uint32_t delta;
div_t result;
uint8_t min = 0;
uint8_t sec = 0;
char kind;
if (state->kind == tomato_break) {
kind = 'b';
} else {
kind = 'f';
}
switch (state->mode) {
case tomato_run:
delta = state->target_ts - state->now_ts;
result = div(delta, 60);
min = result.quot;
sec = result.rem;
break;
case tomato_ready:
min = get_length(state);
sec = 0;
break;
}
if (state->visible) {
sprintf(buf, "TO %c%2d%02d%2d", kind, min, sec, state->done_count);
watch_display_string(buf, 0);
}
}
static void tomato_reset(tomato_state_t *state) {
state->mode = tomato_ready;
movement_cancel_background_task();
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
static void tomato_ring(tomato_state_t *state) {
movement_play_signal();
tomato_reset(state);
if (state->kind == tomato_focus) {
state->kind = tomato_break;
state->done_count++;
} else {
state->kind = tomato_focus;
}
}
void tomato_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(tomato_state_t));
tomato_state_t *state = (tomato_state_t*)*context_ptr;
memset(*context_ptr, 0, sizeof(tomato_state_t));
state->mode=tomato_ready;
state->kind= tomato_focus;
state->done_count = 0;
state->visible = true;
}
}
void tomato_face_activate(void *context) {
tomato_state_t *state = (tomato_state_t *)context;
if (state->mode == tomato_run) {
watch_date_time_t now = watch_rtc_get_date_time();
state->now_ts = watch_utility_date_time_to_unix_time(now, movement_get_current_timezone_offset());
watch_set_indicator(WATCH_INDICATOR_BELL);
}
watch_set_colon();
state->visible = true;
}
bool tomato_face_loop(movement_event_t event, void *context) {
tomato_state_t *state = (tomato_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
tomato_draw(state);
break;
case EVENT_TICK:
if (state->mode == tomato_run) {
state->now_ts++;
}
tomato_draw(state);
break;
case EVENT_LIGHT_BUTTON_DOWN:
movement_illuminate_led();
if (state->mode == tomato_ready) {
if (state->kind == tomato_break) {
state->kind = tomato_focus;
} else {
state->kind = tomato_break;
}
}
tomato_draw(state);
break;
case EVENT_ALARM_BUTTON_UP:
switch(state->mode) {
case tomato_run:
tomato_reset(state);
break;
case tomato_ready:
tomato_start(state);
break;
}
tomato_draw(state);
break;
case EVENT_ALARM_LONG_PRESS:
state->done_count = 0;
break;
case EVENT_BACKGROUND_TASK:
tomato_ring(state);
tomato_draw(state);
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void tomato_face_resign(void *context) {
tomato_state_t *state = (tomato_state_t *)context;
state->visible = false;
(void) context;
}

View File

@@ -0,0 +1,84 @@
/*
* MIT License
*
* Copyright (c) 2022 Wesley Ellis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef TOMATO_FACE_H_
#define TOMATO_FACE_H_
/*
* TOMATO TIMER face
*
* Add a "tomato" timer watch face that alternates between 25 and 5 minute
* timers as in the Pomodoro Technique.
* https://en.wikipedia.org/wiki/Pomodoro_Technique
*
* The top right letter shows mode (f for focus or b for break).
* The bottom right shows how many focus sessions you've completed.
* (You can reset the count with a long press of alarm)
*
* When you show up and it says 25 minutes, you can start it (alarm),
* switch to 5 minute (light) mode or leave (mode).
*
* When it's running you can reset (alarm), or leave (mode).
*
* When it's done, we beep and go back to step 1, changing switching
* mode from focus to break (or break to focus)
*/
#include "movement.h"
typedef enum {
tomato_ready,
tomato_run,
// to_pause, // TODO implement pausing
} tomato_mode;
typedef enum {
tomato_break,
tomato_focus,
} tomato_kind;
typedef struct {
uint32_t target_ts;
uint32_t now_ts;
tomato_mode mode;
tomato_kind kind;
uint8_t done_count;
bool visible;
} tomato_state_t;
void tomato_face_setup(uint8_t watch_face_index, void ** context_ptr);
void tomato_face_activate(void *context);
bool tomato_face_loop(movement_event_t event, void *context);
void tomato_face_resign(void *context);
#define tomato_face ((const watch_face_t){ \
tomato_face_setup, \
tomato_face_activate, \
tomato_face_loop, \
tomato_face_resign, \
NULL, \
})
#endif // TOMATO_FACE_H_

View File

@@ -0,0 +1,791 @@
/*
* MIT License
*
* Copyright (c) 2023 Tobias Raayoni Last / @randogoth
*
* 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 "toss_up_face.h"
#if __EMSCRIPTEN__
#include <time.h>
#else
#include "saml22j18a.h"
#endif
static const char heads[] = { '8', 'h', '4', 'E', '(' };
static const char tails[] = { '0', '+', 'N', '3', ')' };
static const uint8_t dd[] = {2, 4, 6, 8, 10,12,20,24,30,32,36,48,99};
static void _roll_dice_multiple(char* result, uint8_t* dice, uint8_t num_dice);
static void _sort_coins(char* token, uint8_t num_bits, uint8_t bits, char* heads, char* tails);
void _display_coins(char* token, bool* bit_array, uint8_t length, toss_up_state_t *state);
static void _toss_up_face_display(toss_up_state_t *state);
static void _dice_animation(toss_up_state_t *state);
static void _coin_animation(toss_up_state_t *state);
// PUBLIC FUNCTIONS ///////////////////////////////////////////////////////////
void toss_up_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(toss_up_state_t));
memset(*context_ptr, 0, sizeof(toss_up_state_t));
toss_up_state_t *state = (toss_up_state_t *)*context_ptr;
// defaults
state->coin_num = 1;
state->dice_num = 1;
state->dice_sides[0] = 6;
state->dice_sides[1] = 6;
state->dice_sides[2] = 6;
state->coin_style[0] = '8';
state->coin_style[1] = '0';
}
}
void toss_up_face_activate(void *context) {
(void) context;
}
bool toss_up_face_loop(movement_event_t event, void *context) {
toss_up_state_t *state = (toss_up_state_t *)context;
uint8_t i = 0;
switch (event.event_type) {
case EVENT_ACTIVATE:
watch_display_string(" Coins ", 0);
break;
case EVENT_TICK:
if ( state->animate ) {
state->animation = (state->animation + 1);
_toss_up_face_display(state);
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_BUTTON_UP:
if ( state->animate ) break;
// change between coins and dice
if ( state->mode <= 1 ) state->mode = 2;
else if ( state->mode >= 2 ) state->mode = 0;
_toss_up_face_display(state);
break;
case EVENT_ALARM_BUTTON_UP:
// toss
if ( state->animate ) break;
switch (state->mode) {
case 0:
state->mode++;
// fall through
case 1:
state->animate = true;
for (i = 0; i < state->coin_num; i++) {
state->coins[i] = divine_bit();
}
break;
case 2:
state->mode++;
// fall through
case 3:
state->animate = true;
for (i = 0; i < state->dice_num; i++) {
state->dice[i] = roll_dice(state->dice_sides[i]);
}
break;
default:
break;
}
_toss_up_face_display(state);
break;
case EVENT_LIGHT_LONG_PRESS:
if ( state->animate ) break;
state->animate = false;
switch (state->mode) {
case 0: // change to default coin style
state->coin_style[0] = heads[0];
state->coin_style[1] = tails[0];
state->coinface = 0;
break;
case 1: // change the coin style
state->coinface = (state->coinface + 1) % 5;
state->coin_style[0] = heads[state->coinface];
state->coin_style[1] = tails[state->coinface];
break;
case 2: // change to default dice sides
state->dice_sides[0] = 6;
state->dice_sides[1] = 6;
state->dice_sides[2] = 6;
state->dd = 0;
break;
case 3: // change the sides of the dice
state->dd = (state->dd + 1) % 13;
state->dice_sides[state->dice_num-1] = dd[state->dd];
state->dice[state->dice_num-1] = dd[state->dd];
break;
default:
break;
}
_toss_up_face_display(state);
break;
case EVENT_ALARM_LONG_PRESS:
if ( state->animate ) break;
state->animate = false;
switch (state->mode) {
case 0: // back to one coin
state->coin_num = 1;
break;
case 1: // up to 6 coins total
state->coin_num = (state->coin_num % 6) + 1;
break;
case 2: // back to one dice
state->dice_num = 1;
break;
case 3: // add up to 3 dice total
state->dice_num = (state->dice_num % 3) + 1;
state->dd = 0;
break;
default:
break;
}
_toss_up_face_display(state);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void toss_up_face_resign(void *context) {
(void) context;
}
// STATIC FUNCTIONS ///////////////////////////////////////////////////////////
/** @brief handles the display
*/
static void _toss_up_face_display(toss_up_state_t *state) {
char buf[11] = {0};
char token[7] = {0};
switch ( state->mode ) {
case 0: // coins title
sprintf(buf, " Coins ");
break;
case 1: // coins divination
_coin_animation(state);
if ( !state->animate ) {
watch_clear_display();
_display_coins(token, state->coins, state->coin_num, state);
sprintf(buf, " %s", token);
}
break;
case 2: // dice title
sprintf(buf, " Dice ");
break;
case 3: // dice divination
_dice_animation(state);
if ( !state->animate ) {
_roll_dice_multiple(token, state->dice, state->dice_num + 1);
sprintf(buf, " %s", token);
}
break;
default:
break;
}
watch_display_string(buf, 0);
}
/** @brief divination method to derive a bit from 32 TRNG bits
*/
uint8_t divine_bit(void) {
uint32_t stalks;
do { // modulo bias filter
stalks = get_true_entropy(); // get 32 TRNG bits as stalks
} while (stalks >= INT32_MAX || stalks <= 0);
uint8_t pile1_xor = 0;
uint8_t pile2_xor = 0;
// Divide the stalks into two piles, alternating ends
for (uint8_t i = 0; i < 16; i++) {
uint8_t left_bit = (stalks >> (31 - 2*i)) & 1;
uint8_t right_bit = (stalks >> (30 - 2*i)) & 1;
if (i % 2 == 0) {
pile1_xor ^= left_bit;
pile2_xor ^= right_bit;
} else {
pile1_xor ^= right_bit;
pile2_xor ^= left_bit;
}
}
// Take the XOR of the pile results
uint8_t result_xor = pile1_xor ^ pile2_xor;
// Output 1 if result_xor is 1, 0 otherwise
return result_xor;
}
/** @brief get 32 True Random Number bits
*/
uint32_t get_true_entropy(void) {
#if __EMSCRIPTEN__
return rand() % INT32_MAX;
#else
hri_mclk_set_APBCMASK_TRNG_bit(MCLK);
hri_trng_set_CTRLA_ENABLE_bit(TRNG);
while (!hri_trng_get_INTFLAG_reg(TRNG, TRNG_INTFLAG_DATARDY)); // Wait for TRNG data to be ready
watch_disable_TRNG();
hri_mclk_clear_APBCMASK_TRNG_bit(MCLK);
return hri_trng_read_DATA_reg(TRNG); // Read a single 32-bit word from TRNG and return it
#endif
}
// COIN FUNCTIONS /////////////////////////////////////////////////////////////
/** @brief sort tossed coins into a pile of heads and a pile of tails
*/
static void _sort_coins(char* token, uint8_t num_bits, uint8_t bits, char* heads, char* tails) {
uint8_t num_ones = 0;
for (uint8_t i = 0; i < num_bits; i++) {
if ((bits >> i) & 1) {
*token++ = *heads;
num_ones++;
}
}
if ( num_bits < 6 ) {
for (uint8_t i = 0; i < (6 - num_bits); i++) {
*token++ = ' ';
}
}
for (uint8_t i = 0; i < (num_bits - num_ones); i++) {
*token++ = *tails;
}
}
/** @brief convert bool array of coinflips to integer for sorting
*/
void _display_coins(char* token, bool* bit_array, uint8_t length, toss_up_state_t *state) {
uint8_t bits = 0;
for (uint8_t i = 0; i < length; i++) {
if (bit_array[i]) {
bits |= (1 << (length - 1 - i));
}
}
_sort_coins(token, length, bits, &state->coin_style[0], &state->coin_style[1]);
}
/** @brief coin animation
*/
static void _coin_animation(toss_up_state_t *state) {
bool heads = false;
bool tails = false;
for (uint8_t i = 0; i < state->coin_num; i++) {
if (state->coins[i] == true) {
heads = true;
} else {
tails = true;
}
}
movement_request_tick_frequency(32);
switch ( state->animation ) {
case 0:
watch_display_string(" ", 4);
if ( heads ) {
watch_set_pixel(0, 18);
watch_set_pixel(2, 18);
} else {
state->animation = 12;
}
break;
case 1:
if ( heads ) {
watch_set_pixel(1, 18);
}
break;
case 2:
if ( heads ) {
watch_set_pixel(0, 19);
watch_set_pixel(2, 19);
}
break;
case 3:
if ( heads ) {
watch_clear_pixel(0, 18);
watch_clear_pixel(2, 18);
}
break;
case 4:
if ( heads ) {
watch_clear_pixel(1, 18);
}
break;
case 5:
if ( heads ) {
watch_clear_pixel(0, 19);
watch_clear_pixel(2, 19);
watch_set_pixel(1, 17);
watch_set_pixel(0, 20);
}
break;
case 6:
if ( heads ) {
watch_set_pixel(2, 20);
watch_set_pixel(0, 21);
}
break;
case 7:
if ( heads ) {
watch_set_pixel(1, 21);
watch_set_pixel(2, 21);
}
break;
case 8:
if ( heads ) {
watch_clear_pixel(1, 17);
watch_clear_pixel(0, 20);
}
break;
case 9:
if ( heads ) {
watch_clear_pixel(2, 20);
watch_clear_pixel(0, 21);
}
break;
case 10:
if ( heads ) {
watch_clear_pixel(1, 21);
watch_clear_pixel(2, 21);
watch_set_pixel(1, 22);
watch_set_pixel(2, 22);
}
break;
case 11:
if ( heads ) {
watch_set_pixel(0, 22);
}
break;
case 12:
if ( heads ) {
watch_set_pixel(2, 23);
watch_set_pixel(0, 23);
}
if ( tails ) {
watch_set_pixel(0, 18);
watch_set_pixel(2, 18);
}
break;
case 13:
if ( heads ) {
watch_clear_pixel(1, 22);
watch_clear_pixel(2, 22);
}
if ( tails ) {
watch_set_pixel(1, 18);
}
break;
case 14:
if ( heads ) {
watch_clear_pixel(0, 22);
}
if ( tails ) {
watch_set_pixel(0, 19);
watch_set_pixel(2, 19);
}
break;
case 15:
if ( heads ) {
watch_clear_pixel(2, 23);
watch_clear_pixel(0, 23);
watch_set_pixel(2, 0);
watch_set_pixel(1, 0);
}
if ( tails ) {
watch_clear_pixel(0, 18);
watch_clear_pixel(2, 18);
}
break;
case 16:
if ( heads ) {
watch_set_pixel(2, 1);
watch_set_pixel(0, 0);
}
if ( tails ) {
watch_clear_pixel(1, 18);
}
break;
case 17:
if ( heads ) {
watch_set_pixel(2, 10);
watch_set_pixel(0, 1);
}
if ( tails ) {
watch_clear_pixel(0, 19);
watch_clear_pixel(2, 19);
watch_set_pixel(1, 17);
watch_set_pixel(0, 20);
}
break;
case 18:
if ( heads ) {
watch_clear_pixel(2, 0);
watch_clear_pixel(1, 0);
}
if ( tails ) {
watch_set_pixel(2, 20);
watch_set_pixel(0, 21);
}
break;
case 19:
if ( heads ) {
watch_clear_pixel(2, 1);
watch_clear_pixel(0, 0);
}
if ( tails ) {
watch_set_pixel(1, 21);
watch_set_pixel(2, 21);
}
break;
case 20:
if ( heads ) {
watch_set_pixel(2, 1);
watch_set_pixel(0, 0);
}
if ( tails ) {
watch_clear_pixel(1, 17);
watch_clear_pixel(0, 20);
}
break;
case 21:
if ( heads ) {
watch_set_pixel(2, 0);
watch_set_pixel(1, 0);
}
if ( tails ) {
watch_clear_pixel(2, 20);
watch_clear_pixel(0, 21);
}
break;
case 22:
if ( heads ) {
watch_clear_pixel(2, 10);
watch_clear_pixel(0, 1);
}
if ( tails ) {
watch_clear_pixel(1, 21);
watch_clear_pixel(2, 21);
watch_set_pixel(1, 22);
watch_set_pixel(2, 22);
}
break;
case 23:
if ( heads ) {
watch_clear_pixel(2, 1);
watch_clear_pixel(0, 0);
}
if ( tails ) {
watch_set_pixel(0, 22);
}
break;
case 24:
if ( heads ) {
watch_set_pixel(2, 23);
watch_set_pixel(0, 23);
watch_clear_pixel(2, 0);
watch_clear_pixel(1, 0);
}
if ( tails ) {
watch_set_pixel(2, 23);
watch_set_pixel(0, 23);
}
break;
case 25:
if ( heads ) {
watch_set_pixel(0, 22);
}
if ( tails ) {
watch_clear_pixel(1, 22);
watch_clear_pixel(2, 22);
}
break;
case 26:
if ( heads ) {
watch_set_pixel(1, 22);
watch_set_pixel(2, 22);
}
if ( tails ) {
watch_clear_pixel(0, 22);
}
break;
case 27:
if ( heads ) {
watch_clear_pixel(2, 23);
watch_clear_pixel(0, 23);
}
if ( tails ) {
watch_clear_pixel(2, 23);
watch_clear_pixel(0, 23);
watch_set_pixel(2, 0);
watch_set_pixel(1, 0);
}
break;
case 28:
if ( heads ) {
watch_clear_pixel(0, 22);
}
if ( tails ) {
watch_set_pixel(2, 1);
watch_set_pixel(0, 0);
}
break;
case 29:
if ( heads ) {
watch_set_pixel(1, 21);
watch_set_pixel(2, 21);
watch_clear_pixel(1, 22);
watch_clear_pixel(2, 22);
}
if ( tails ) {
watch_set_pixel(2, 10);
watch_set_pixel(0, 1);
}
break;
case 30:
if ( heads ) {
watch_set_pixel(2, 20);
watch_set_pixel(0, 21);
}
if ( tails ) {
watch_clear_pixel(1, 0);
watch_clear_pixel(2, 0);
}
break;
case 31:
if ( heads ) {
watch_set_pixel(1, 17);
watch_set_pixel(0, 20);
}
if ( tails ) {
watch_clear_pixel(2, 1);
watch_clear_pixel(0, 0);
}
break;
case 32:
if ( heads ) {
watch_clear_pixel(1, 21);
watch_clear_pixel(2, 21);
}
if ( tails ) {
watch_clear_pixel(2, 10);
watch_clear_pixel(0, 1);
watch_set_pixel(0, 2);
watch_set_pixel(1, 2);
}
break;
case 33:
if ( heads ) {
watch_clear_pixel(2, 20);
watch_clear_pixel(0, 21);
}
if ( tails ) {
watch_set_pixel(2, 2);
watch_set_pixel(0, 3);
}
break;
case 34:
if ( heads ) {
watch_set_pixel(0, 19);
watch_set_pixel(2, 19);
watch_clear_pixel(1, 17);
watch_clear_pixel(0, 20);
}
if ( tails ) {
watch_set_pixel(2, 3);
watch_set_pixel(0, 4);
}
break;
case 35:
if ( heads ) {
watch_set_pixel(1, 18);
}
if ( tails ) {
watch_clear_pixel(1, 2);
watch_clear_pixel(0, 2);
}
break;
case 36:
if ( heads ) {
watch_set_pixel(0, 18);
watch_set_pixel(2, 18);
}
if ( tails ) {
watch_clear_pixel(2, 2);
watch_clear_pixel(0, 3);
}
break;
case 37:
if ( heads ) {
watch_clear_pixel(0, 19);
watch_clear_pixel(2, 19);
}
if ( tails ) {
watch_clear_pixel(2, 3);
watch_clear_pixel(0, 4);
watch_set_pixel(1, 4);
watch_set_pixel(0, 5);
}
break;
case 38:
if ( heads ) {
watch_clear_pixel(1, 18);
}
if ( tails ) {
watch_set_pixel(2, 4);
watch_set_pixel(0, 6);
}
break;
case 39:
if ( heads ) {
watch_clear_pixel(0, 18);
watch_clear_pixel(2, 18);
}
if ( tails ) {
watch_set_pixel(1, 6);
watch_set_pixel(2, 5);
}
state->animate = false;
state->animation = 0;
movement_request_tick_frequency(1);
}
}
// DICE FUNCTIONS /////////////////////////////////////////////////////////////
/** @brief rolls a dice
*/
uint8_t roll_dice(uint8_t sides) {
uint8_t bits_needed = 0;
uint8_t temp_sides = sides - 1;
uint8_t result = 0;
while (temp_sides > 0) {
bits_needed++; // how many bits do we need to represent this number?
temp_sides >>= 1; // Shift right to check the next bit
}
do {
result = 0;
for (int i = 0; i < bits_needed; i++) {
result <<= 1; // Shift left to make room for the next bit
result |= divine_bit(); // Add the next bit to the result
}
} while ( result > sides -1 );
return result + 1; // Add 1 to convert the range from 0 to sides-1 to 1 to sides
}
/** @brief roll multiple dice and print a char array for displaying them
*/
static void _roll_dice_multiple(char* result, uint8_t* dice, uint8_t num_dice) {
// initialize the result array to all spaces
memset(result, ' ', 6);
// roll the dice and write the result to the result array
for (uint8_t i = 0; i < num_dice-1; i++) {
uint8_t dice_result = dice[i];
uint8_t tens_digit = dice_result / 10;
uint8_t ones_digit = dice_result % 10;
result[(i * 2)] = tens_digit == 0 ? ' ' : (char)('0' + tens_digit);
result[(i * 2) + 1] = (char)('0' + ones_digit);
}
}
/** @brief dice animation
*/
static void _dice_animation(toss_up_state_t *state) {
watch_display_string(" ", 4);
for (uint8_t i = 0; i < state->dice_num; i++) {
watch_display_string("0",i*2 + 5);
}
movement_request_tick_frequency(16);
switch ( state->animation ) {
case 0:
watch_clear_pixel(1, 17);
watch_clear_pixel(0, 0);
watch_clear_pixel(1, 6);
break;
case 1:
watch_clear_pixel(2, 20);
watch_clear_pixel(1, 0);
watch_clear_pixel(0, 6);
break;
case 2:
watch_clear_pixel(2, 21);
watch_clear_pixel(2, 0);
watch_clear_pixel(0, 5);
break;
case 3:
watch_clear_pixel(1, 21);
watch_clear_pixel(2, 1);
watch_clear_pixel(1, 4);
break;
case 4:
watch_clear_pixel(0, 21);
watch_clear_pixel(2, 10);
watch_clear_pixel(2, 4);
break;
case 5:
watch_clear_pixel(0, 20);
watch_clear_pixel(0, 1);
watch_clear_pixel(2, 5);
break;
case 6:
watch_clear_pixel(1, 17);
watch_clear_pixel(0, 0);
watch_clear_pixel(1, 6);
break;
case 7:
watch_clear_pixel(2, 20);
watch_clear_pixel(1, 0);
watch_clear_pixel(0, 6);
break;
case 8:
watch_clear_pixel(2, 21);
watch_clear_pixel(2, 0);
watch_clear_pixel(0, 5);
break;
case 9:
watch_clear_pixel(1, 21);
watch_clear_pixel(2, 1);
watch_clear_pixel(1, 4);
break;
case 10:
watch_clear_pixel(0, 21);
watch_clear_pixel(2, 10);
watch_clear_pixel(2, 4);
break;
case 11:
watch_clear_pixel(0, 20);
watch_clear_pixel(0, 1);
watch_clear_pixel(2, 5);
state->animate = false;
state->animation = 0;
movement_request_tick_frequency(1);
}
}

View File

@@ -0,0 +1,112 @@
/*
* MIT License
*
* Copyright (c) 2023 Tobias Raayoni Last / @randogoth
*
* 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 TOSS_UP_FACE_H_
#define TOSS_UP_FACE_H_
/*
* TOSS UP face
* ============
*
* Playful watch face for games of chance or divination using coins or dice.
*
* LIGHT switches between Coins and Dice mode
*
* COINS
* =====
*
* ALARM tosses a coin. If it lands on heads it gets sorted to the left side of the
* display, if it lands on tails then sorted to the right side.
*
* LONG PRESSING ALARM adds up to 5 more coins to the toss for more nuance in the decision
* making (e.g. three heads vs two tails could be read as "yes, but with serious doubts").
*
* LONG PRESSING LIGHT flips through additional style for the coins from the default Ө/O
* to H/T (heads/tails), Y/N (yes/no), E/Ǝ, C/Ↄ
*
* LONG PRESSING ALARM on the "Coins" title page resets to one coin.
* LONG PRESSING LIGHT on the "Coins" title page resets the style to Ө/O
*
* DICE
* ====
*
* ALARM rolls a six sided dice.
*
* LONG PRESSING ALARM adds up to 2 more dice to the roll.
*
* LONG PRESSING LIGHT flips through other available polyhedral dice types with less or more
* than the default 6 sides. The options are D2, D4, D6, D8, D10, D12, D20, D24, D30, D32, D36,
* D48, and a hypothetical D99.
*
* When more than one dice is used for a roll this changes only the last added dice. (see Note
* below)
*
* LONG PRESSING ALARM on the "Dice" title page resets to one dice.
* LONG PRESSING LIGHT on the "Dice" title page resets the dice to D6.
*
* Please Note: If you need let's say a D8, D12, and D20 for your rolls then the procedure to
* set this up would be as follows: from the default screen where you can roll the one D6 dice
* you would LONG PRESS LIGHT a few times to change the D6 to a D8, then LONG PRESS ALARM to add
* a second dice, LONG PRESS LIGHT again until the second dice changes to D12, then LONG PRESS
* ALARM to add the third dice and LONG PRESS LIGHT again a few times until it becomes a D20.
*
*/
#include "movement.h"
typedef struct {
// Anything you need to keep track of, put it here!
uint32_t entropy;
uint8_t mode : 4; // 1 coin, 2 coins, 3 coins, 4 coins, dice, iching, geomnc
bool setup;
bool coins[6];
uint8_t coin_num : 3;
char coin_style[2];
uint8_t coinface : 3;
uint8_t dice[3];
uint8_t dice_num : 2;
uint8_t dd : 6;
uint8_t dice_sides[3];
uint8_t animation;
bool animate;
} toss_up_state_t;
uint32_t get_true_entropy(void);
uint8_t divine_bit(void);
uint8_t roll_dice(uint8_t sides);
void toss_up_face_setup(uint8_t watch_face_index, void ** context_ptr);
void toss_up_face_activate(void *context);
bool toss_up_face_loop(movement_event_t event, void *context);
void toss_up_face_resign(void *context);
#define toss_up_face ((const watch_face_t){ \
toss_up_face_setup, \
toss_up_face_activate, \
toss_up_face_loop, \
toss_up_face_resign, \
NULL, \
})
#endif // TOSS_UP_FACE_H_

Some files were not shown because too many files have changed in this diff Show More