Merge branch 'main' into tide_face

This commit is contained in:
voloved
2026-04-01 16:40:57 -04:00
committed by GitHub
77 changed files with 5915 additions and 1432 deletions
+236
View File
@@ -0,0 +1,236 @@
/*
* MIT License
*
* Copyright (c) 2025 Raffael Mancini
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Solar time formulas follow the notation from:
* https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-time
*/
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "solar_time_face.h"
#include "watch.h"
#include "watch_utility.h"
#include "filesystem.h"
#if __EMSCRIPTEN__
#include <emscripten.h>
#endif
#ifndef M_PI
#define M_PI 3.14159265358979323846f
#endif
/* ---------------------------------------------------------------------------
* Solar time math (pveducation.org notation)
* ---------------------------------------------------------------------------
*
* LSTM = 15 * ΔTUTC [degrees]
* B = (360 / 365) * (d - 81) [degrees] d = day-of-year
* EoT = 9.87*sin(2B) - 7.53*cos(B)
* - 1.5*sin(B) [minutes]
* TC = 4 * (Longitude - LSTM) + EoT [minutes]
* LST = LT + TC/60 [hours]
* HRA = 15 * (LST - 12) [degrees]
* ---------------------------------------------------------------------------
*/
static movement_location_t _load_location(void) {
movement_location_t loc = {0};
filesystem_read_file("location.u32", (char *)&loc.reg, sizeof(loc.reg));
return loc;
}
/* Compute and cache EoT and TC. Call when d != state->last_calc_d. */
static void _compute_daily(solar_time_state_t *state, uint16_t d) {
/* LSTM — movement_get_current_timezone_offset() returns seconds from UTC */
float delta_T_UTC = (float)movement_get_current_timezone_offset() / 3600.0f;
float LSTM = 15.0f * delta_T_UTC;
movement_location_t loc = _load_location();
float longitude = (float)(int16_t)loc.bit.longitude / 100.0f;
/* B in radians for sinf/cosf */
float B = (360.0f / 365.0f) * ((float)d - 81.0f) * ((float)M_PI / 180.0f);
state->EoT = 9.87f * sinf(2.0f * B) - 7.53f * cosf(B) - 1.5f * sinf(B);
state->TC = 4.0f * (longitude - LSTM) + state->EoT;
state->last_calc_d = d;
}
/* Recompute if the day-of-year has rolled over. Returns current d. */
static uint16_t _maybe_recompute(solar_time_state_t *state, watch_date_time_t dt) {
uint16_t d = watch_utility_days_since_new_year(
(uint16_t)(dt.unit.year + WATCH_RTC_REFERENCE_YEAR),
dt.unit.month,
dt.unit.day
);
if (d != state->last_calc_d && _load_location().reg != 0) {
_compute_daily(state, d);
}
return d;
}
/* LST as total seconds since midnight (0..86399).
* LST = LT + TC/60 => in seconds: LT_sec + TC*60 */
static int32_t _lst_seconds(watch_date_time_t dt, float TC) {
int32_t lt = (int32_t)dt.unit.hour * 3600
+ (int32_t)dt.unit.minute * 60
+ (int32_t)dt.unit.second;
int32_t tc = (int32_t)(TC * 60.0f);
return ((lt + tc) % 86400 + 86400) % 86400;
}
static void _update_display(solar_time_state_t *state, watch_date_time_t dt) {
char bottom[7];
if (_load_location().reg == 0) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SOL", "SO");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, "no Loc");
watch_clear_colon();
return;
}
switch (state->mode) {
case SOLAR_TIME_MODE_LST: {
int32_t s = _lst_seconds(dt, state->TC);
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SOL", "SO");
watch_display_text(WATCH_POSITION_TOP_RIGHT, "Ar");
sprintf(bottom, "%02d%02d%02d",
(int)(s / 3600), (int)((s % 3600) / 60), (int)(s % 60));
watch_set_colon();
break;
}
case SOLAR_TIME_MODE_NOON: {
/* Solar noon: moment when LST = 12:00 → LT_noon = 12h - TC/60 */
int32_t s = (int32_t)(( 12.0f - state->TC / 60.0f) * 3600.0f);
s = ((s % 86400) + 86400) % 86400;
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "NOO", "NO");
watch_display_text(WATCH_POSITION_TOP_RIGHT, "n ");
sprintf(bottom, "%02d%02d ", (int)(s / 3600), (int)((s % 3600) / 60));
watch_set_colon();
break;
}
case SOLAR_TIME_MODE_HRA: {
/* HRA = 15 * (LST - 12); negative = morning, positive = afternoon */
int32_t s = _lst_seconds(dt, state->TC);
int16_t hra = (int16_t)roundf(15.0f * ((float)s / 3600.0f - 12.0f));
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "HrA", "Hr");
watch_display_text(WATCH_POSITION_TOP_RIGHT, "n ");
sprintf(bottom, "%+4d ", (int)hra);
watch_clear_colon();
break;
}
default:
return;
}
watch_display_text(WATCH_POSITION_BOTTOM, bottom);
}
/* ---- Movement callbacks -------------------------------------------------- */
void solar_time_face_setup(uint8_t watch_face_index, void **context_ptr) {
(void)watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(solar_time_state_t));
memset(*context_ptr, 0, sizeof(solar_time_state_t));
/* last_calc_d == 0 guarantees recomputation on first tick */
}
}
void solar_time_face_activate(void *context) {
solar_time_state_t *state = (solar_time_state_t *)context;
#if __EMSCRIPTEN__
/* In the simulator the browser exposes lat/lon as JS globals.
* Write them to location.u32 if not already set. */
int16_t browser_lat = EM_ASM_INT({ return lat; });
int16_t browser_lon = EM_ASM_INT({ return lon; });
if (browser_lat || browser_lon) {
movement_location_t browser_loc = {0};
filesystem_read_file("location.u32", (char *)&browser_loc.reg, sizeof(browser_loc.reg));
if (browser_loc.reg == 0) {
browser_loc.bit.latitude = browser_lat;
browser_loc.bit.longitude = browser_lon;
filesystem_write_file("location.u32", (char *)&browser_loc.reg, sizeof(browser_loc.reg));
}
}
#endif
/* Force recompute on activation: timezone or location may have changed */
state->last_calc_d = 0;
watch_date_time_t dt = movement_get_local_date_time();
_maybe_recompute(state, dt);
}
bool solar_time_face_loop(movement_event_t event, void *context) {
solar_time_state_t *state = (solar_time_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
case EVENT_TICK: {
watch_date_time_t dt = movement_get_local_date_time();
_maybe_recompute(state, dt);
_update_display(state, dt);
break;
}
case EVENT_ALARM_BUTTON_UP:
state->mode = (solar_time_mode_t)((state->mode + 1) % SOLAR_TIME_NUM_MODES);
{
watch_date_time_t dt = movement_get_local_date_time();
_update_display(state, dt);
}
break;
case EVENT_LOW_ENERGY_UPDATE: {
if (!watch_sleep_animation_is_running()) watch_start_sleep_animation(1000);
watch_date_time_t dt = movement_get_local_date_time();
_maybe_recompute(state, dt);
_update_display(state, dt);
break;
}
case EVENT_TIMEOUT:
state->mode = SOLAR_TIME_MODE_LST;
if (_load_location().reg == 0) movement_move_to_face(0);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void solar_time_face_resign(void *context) {
solar_time_state_t *state = (solar_time_state_t *)context;
state->mode = SOLAR_TIME_MODE_LST;
watch_clear_colon();
}
+84
View File
@@ -0,0 +1,84 @@
/*
* MIT License
*
* Copyright (c) 2025 Raffael Mancini
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
/*
* SOLAR TIME FACE
*
* Displays solar time information based on the user's location.
* Formulas follow the notation from:
* https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-time
*
* Variables (pveducation.org notation):
* LSTM - Local Standard Time Meridian [degrees] = 15 * ΔTUTC
* B - Seasonal angle [degrees] = (360/365) * (d - 81)
* EoT - Equation of Time [minutes] = 9.87*sin(2B) - 7.53*cos(B) - 1.5*sin(B)
* TC - Time Correction Factor [minutes] = 4*(Longitude - LSTM) + EoT
* LST - Solar Time [hours] = LT + TC/60
* HRA - Hour Angle [degrees] = 15*(LST - 12)
*
* B, EoT and TC only depend on the day-of-year d, so they are cached
* in state and recomputed exactly once per day: the cache key is d itself
* (1-366). Zero-initialisation of state guarantees a recompute on first use.
*
* Requires the location to be set via the Sunrise/Sunset face, stored in
* "location.u32" on the filesystem. If no location is set, displays
* "SO no Loc".
*
* Display modes (cycle with the Alarm / start-stop button):
* SO HH:MM:SS — Solar Time (LST), live seconds display
* nO HH:MM — Solar Noon in local clock time
* Hr ±DDD — Hour Angle (HRA) in degrees; negative=morning, positive=afternoon
*/
#include "movement.h"
typedef enum {
SOLAR_TIME_MODE_LST = 0, /* Solar Time SO HH:MM:SS */
SOLAR_TIME_MODE_NOON = 1, /* Solar Noon (local) nO HH:MM */
SOLAR_TIME_MODE_HRA = 2, /* Hour Angle Hr ±DDD */
SOLAR_TIME_NUM_MODES
} solar_time_mode_t;
typedef struct {
solar_time_mode_t mode;
uint16_t last_calc_d; /* day-of-year (1-366) when EoT/TC were last computed;
0 (zero-init) guarantees recomputation on first tick */
float EoT; /* Equation of Time [minutes] */
float TC; /* Time Correction Factor [minutes] */
} solar_time_state_t;
void solar_time_face_setup(uint8_t watch_face_index, void **context_ptr);
void solar_time_face_activate(void *context);
bool solar_time_face_loop(movement_event_t event, void *context);
void solar_time_face_resign(void *context);
#define solar_time_face ((const watch_face_t){ \
solar_time_face_setup, \
solar_time_face_activate, \
solar_time_face_loop, \
solar_time_face_resign, \
NULL, \
})
+49 -25
View File
@@ -40,9 +40,9 @@ typedef enum {
alarm_setting_idx_beeps
} alarm_setting_idx_t;
static const char _dow_strings[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SO", "ED", "1t", "MF", "WN"};
static const uint8_t _blink_idx[ALARM_SETTING_STATES] = {2, 0, 4, 6, 8, 9};
static const uint8_t _blink_idx2[ALARM_SETTING_STATES] = {3, 1, 5, 7, 8, 9};
static const char _dow_strings_classic[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SU", "ED", "1t", "MF", "WN"};
static const char _dow_strings_custom[ALARM_DAY_STATES + 1][3] ={ "AL ", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN", "DAY", "1t ", "M-F", "WKD"};
static const uint8_t _beeps_blink_idx = 9;
static const watch_buzzer_note_t _buzzer_notes[3] = {BUZZER_NOTE_B6, BUZZER_NOTE_C8, BUZZER_NOTE_A8};
// Volume is indicated by the three segments 5D, 5G and 5A
@@ -67,6 +67,10 @@ static void _alarm_set_signal(alarm_state_t *state) {
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
}
static void _alarm_show_alarm_on_text(alarm_state_t *state) {
watch_display_text(WATCH_POSITION_SECONDS, state->alarm[state->alarm_idx].enabled ? "on" : "--");
}
static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) {
char buf[12];
bool set_leading_zero = movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_024H;
@@ -90,18 +94,34 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) {
watch_set_indicator(WATCH_INDICATOR_24H);
}
sprintf(buf, set_leading_zero? "%c%c%2d%02d%02d " : "%c%c%2d%2d%02d ",
_dow_strings[i][0], _dow_strings[i][1],
(state->alarm_idx + 1),
h,
state->alarm[state->alarm_idx].minute);
// blink items if in settings mode
if (state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks) {
buf[_blink_idx[state->setting_state]] = buf[_blink_idx2[state->setting_state]] = ' ';
bool blinking = state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks;
if (state->setting_state == alarm_setting_idx_alarm && blinking) {
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
} else {
sprintf(buf, "%2d", (state->alarm_idx + 1));
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
if (state->setting_state == alarm_setting_idx_day && blinking) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, " ", " ");
} else {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, _dow_strings_custom[i], _dow_strings_classic[i]);
}
if (state->setting_state == alarm_setting_idx_hour && blinking) {
watch_display_text(WATCH_POSITION_HOURS, " ");
} else {
sprintf(buf, set_leading_zero? "%02d" : "%2d", h);
watch_display_text(WATCH_POSITION_HOURS, buf);
}
if (state->setting_state == alarm_setting_idx_minute && blinking) {
watch_display_text(WATCH_POSITION_MINUTES, " ");
} else {
sprintf(buf, "%02d", state->alarm[state->alarm_idx].minute);
watch_display_text(WATCH_POSITION_MINUTES, buf);
}
watch_display_text(WATCH_POSITION_FULL, buf);
if (state->is_setting) {
watch_display_text(WATCH_POSITION_SECONDS, " ");
// draw pitch level indicator
if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_pitch)) {
for (i = 0; i <= state->alarm[state->alarm_idx].pitch && i < 3; i++)
@@ -110,15 +130,18 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) {
// draw beep rounds indicator
if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_beeps)) {
if (state->alarm[state->alarm_idx].beeps == ALARM_MAX_BEEP_ROUNDS - 1)
watch_display_character('L', _blink_idx[alarm_setting_idx_beeps]);
watch_display_character('L', _beeps_blink_idx);
else {
if (state->alarm[state->alarm_idx].beeps == 0)
watch_display_character('o', _blink_idx[alarm_setting_idx_beeps]);
watch_display_character('o', _beeps_blink_idx);
else
watch_display_character(state->alarm[state->alarm_idx].beeps + 48, _blink_idx[alarm_setting_idx_beeps]);
watch_display_character(state->alarm[state->alarm_idx].beeps + 48, _beeps_blink_idx);
}
}
}
else {
_alarm_show_alarm_on_text(state);
}
// set alarm indicator
_alarm_set_signal(state);
@@ -179,9 +202,16 @@ static void _alarm_update_alarm_enabled(alarm_state_t *state) {
static void _alarm_play_short_beep(uint8_t pitch_idx) {
// play a short double beep
watch_buzzer_play_note(_buzzer_notes[pitch_idx], 50);
watch_buzzer_play_note(BUZZER_NOTE_REST, 50);
watch_buzzer_play_note(_buzzer_notes[pitch_idx], 70);
static int8_t beep_sequence[] = {
0, 4,
BUZZER_NOTE_REST, 4,
0, 6,
0
};
beep_sequence[0] = _buzzer_notes[pitch_idx];
beep_sequence[4] = _buzzer_notes[pitch_idx];
movement_play_sequence(beep_sequence, 0);
}
static void _alarm_indicate_beep(alarm_state_t *state) {
@@ -303,6 +333,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) {
// revert change of enabled flag and show it briefly
state->alarm[state->alarm_idx].enabled ^= 1;
_alarm_set_signal(state);
_alarm_show_alarm_on_text(state);
delay_ms(275);
state->alarm_idx = 0;
}
@@ -413,14 +444,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) {
// play alarm
if (state->alarm[state->alarm_playing_idx].beeps == 0) {
// short beep
if (watch_is_buzzer_or_led_enabled()) {
_alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch);
} else {
// enable, play beep and disable buzzer again
watch_enable_buzzer();
_alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch);
watch_disable_buzzer();
}
_alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch);
} else {
// regular alarm beeps
movement_play_alarm_beeps((state->alarm[state->alarm_playing_idx].beeps == (ALARM_MAX_BEEP_ROUNDS - 1) ? 20 : state->alarm[state->alarm_playing_idx].beeps),
+2 -2
View File
@@ -210,7 +210,7 @@ static inline void _display_counts(baby_kicks_state_t *state) {
watch_display_text(WATCH_POSITION_BOTTOM, "baby ");
watch_clear_colon();
} else {
char buf[7];
char buf[9];
snprintf(
buf,
@@ -259,7 +259,7 @@ static void _display_elapsed_minutes(baby_kicks_state_t *state) {
* the total elapsed minutes.
*/
char buf[3];
char buf[5];
uint32_t elapsed_minutes = _elapsed_minutes(state);
uint8_t multiple = elapsed_minutes / 30;
uint8_t remainder = elapsed_minutes % 30;
+472
View File
@@ -0,0 +1,472 @@
/*
* MIT License
*
* Copyright (c) 2025 David Volovskiy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Emulator only: need time() to seed the random number generator.
#if __EMSCRIPTEN__
#include <time.h>
#endif
#include <stdlib.h>
#include <string.h>
#include "blackjack_face.h"
#include "watch_common_display.h"
#define ACE 14
#define KING 13
#define QUEEN 12
#define JACK 11
#define MIN_CARD_VALUE 2
#define MAX_CARD_VALUE ACE
#define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1)
#define CARD_SUIT_COUNT 4
#define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT)
#define BLACKJACK_MAX_HAND_SIZE 11 // 4*1 + 4*2 + 3*3 = 21; 11 cards total
#define MAX_PLAYER_CARDS_DISPLAY 4
#define BOARD_DISPLAY_START 4
typedef struct {
uint8_t score;
uint8_t idx_hand;
int8_t high_aces_in_hand;
uint8_t hand[BLACKJACK_MAX_HAND_SIZE];
} hand_info_t;
typedef enum {
BJ_TITLE_SCREEN,
BJ_PLAYING,
BJ_DEALER_PLAYING,
BJ_BUST,
BJ_RESULT,
BJ_WIN_RATIO,
} game_state_t;
typedef enum {
A, B, C, D, E, F, G
} segment_t;
static bool tap_turned_on = false;
static game_state_t game_state;
static uint8_t deck[DECK_SIZE] = {0};
static uint8_t current_card = 0;
static blackjack_face_state_t *g_state = NULL;
hand_info_t player;
hand_info_t dealer;
static uint8_t generate_random_number(uint8_t num_values) {
// Emulator: use rand. Hardware: use arc4random.
#if __EMSCRIPTEN__
return rand() % num_values;
#else
return arc4random_uniform(num_values);
#endif
}
static void stack_deck(void) {
for (size_t i = 0; i < CARD_RANK_COUNT; i++) {
for (size_t j = 0; j < CARD_SUIT_COUNT; j++)
deck[(i * CARD_SUIT_COUNT) + j] = MIN_CARD_VALUE + i;
}
}
static void shuffle_deck(void) {
// Randomize shuffle with Fisher Yates
size_t i;
size_t j;
uint8_t tmp;
for (i = DECK_SIZE - 1; i > 0; i--) {
j = generate_random_number(0xFF) % (i + 1);
tmp = deck[j];
deck[j] = deck[i];
deck[i] = tmp;
}
}
static void reset_deck(void) {
current_card = 0;
shuffle_deck();
}
static uint8_t get_next_card(void) {
if (current_card >= DECK_SIZE)
reset_deck();
return deck[current_card++];
}
static uint8_t get_card_value(uint8_t card) {
switch (card)
{
case ACE:
return 11;
case KING:
case QUEEN:
case JACK:
return 10;
default:
return card;
}
}
static void modify_score_from_aces(hand_info_t *hand_info) {
while (hand_info->score > 21 && hand_info->high_aces_in_hand > 0) {
hand_info->score -= 10;
hand_info->high_aces_in_hand--;
}
}
static void reset_hands(void) {
memset(&player, 0, sizeof(player));
memset(&dealer, 0, sizeof(dealer));
reset_deck();
}
static void give_card(hand_info_t *hand_info) {
uint8_t card = get_next_card();
if (card == ACE) hand_info->high_aces_in_hand++;
hand_info->hand[hand_info->idx_hand++] = card;
uint8_t card_value = get_card_value(card);
hand_info->score += card_value;
modify_score_from_aces(hand_info);
}
static void set_segment_at_position(segment_t segment, uint8_t position) {
digit_mapping_t segmap;
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) {
segmap = Custom_LCD_Display_Mapping[position];
} else {
segmap = Classic_LCD_Display_Mapping[position];
}
const uint8_t com_pin = segmap.segment[segment].address.com;
const uint8_t seg = segmap.segment[segment].address.seg;
watch_set_pixel(com_pin, seg);
}
static void display_card_at_position(uint8_t card, uint8_t display_position) {
switch (card) {
case KING:
watch_display_character(' ', display_position);
set_segment_at_position(A, display_position);
set_segment_at_position(D, display_position);
set_segment_at_position(G, display_position);
break;
case QUEEN:
watch_display_character(' ', display_position);
set_segment_at_position(A, display_position);
set_segment_at_position(D, display_position);
break;
case JACK:
watch_display_character('-', display_position);
break;
case ACE:
watch_display_character(watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM ? 'A' : 'a', display_position);
break;
case 10:
watch_display_character('0', display_position);
break;
default: {
const char display_char = card + '0';
watch_display_character(display_char, display_position);
break;
}
}
}
static void display_player_hand(void) {
uint8_t card;
if (player.idx_hand <= MAX_PLAYER_CARDS_DISPLAY) {
card = player.hand[player.idx_hand - 1];
display_card_at_position(card, BOARD_DISPLAY_START + player.idx_hand - 1);
} else {
for (uint8_t i=0; i<MAX_PLAYER_CARDS_DISPLAY; i++) {
card = player.hand[player.idx_hand - MAX_PLAYER_CARDS_DISPLAY + i];
display_card_at_position(card, BOARD_DISPLAY_START + i);
}
}
}
static void display_dealer_hand(void) {
uint8_t card = dealer.hand[dealer.idx_hand - 1];
display_card_at_position(card, 0);
}
static void display_score(uint8_t score, watch_position_t pos) {
char buf[4];
sprintf(buf, "%2d", score);
watch_display_text(pos, buf);
}
static void add_to_game_scores(bool add_to_wins) {
g_state->games_played++;
if (g_state->games_played == 0) {
// Overflow
g_state->games_played = 1;
g_state->games_won = add_to_wins ? 1 : 0;
return;
}
if (add_to_wins) {
g_state->games_won++;
if (g_state->games_won == 0) {
// Overflow
g_state->games_played = 1;
g_state->games_won = 1;
}
}
}
static void display_win(void) {
game_state = BJ_RESULT;
add_to_game_scores(true);
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WlN ", " WIN");
display_score(player.score, WATCH_POSITION_SECONDS);
display_score(dealer.score, WATCH_POSITION_TOP_RIGHT);
}
static void display_lose(void) {
game_state = BJ_RESULT;
add_to_game_scores(false);
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "LOSE", "lOSE");
display_score(player.score, WATCH_POSITION_SECONDS);
display_score(dealer.score, WATCH_POSITION_TOP_RIGHT);
}
static void display_tie(void) {
game_state = BJ_RESULT;
// Don't record ties to the win ratio
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "TlE ", " TIE");
display_score(player.score, WATCH_POSITION_SECONDS);
}
static void display_bust(void) {
game_state = BJ_RESULT;
add_to_game_scores(false);
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST", "BUST");
}
static void display_title(void) {
game_state = BJ_TITLE_SCREEN;
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text_with_fallback(WATCH_POSITION_TOP, "BLACK ", "21");
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " JACK ", "BLaKJK");
}
static void display_win_ratio(blackjack_face_state_t *state) {
char buf[7];
game_state = BJ_WIN_RATIO;
uint8_t win_ratio = 0;
if (state->games_played > 0) { // Avoid dividing by zero
win_ratio = (uint8_t)((100 * state->games_won) / state->games_played);
}
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text_with_fallback(WATCH_POSITION_TOP, "WINS ", "WR");
sprintf(buf, "%3dPct", win_ratio);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
static void begin_playing(bool tap_control_on) {
watch_clear_display();
if (tap_control_on) {
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
}
game_state = BJ_PLAYING;
reset_hands();
// Give player their first 2 cards
give_card(&player);
display_player_hand();
give_card(&player);
display_player_hand();
display_score(player.score, WATCH_POSITION_SECONDS);
give_card(&dealer);
display_dealer_hand();
display_score(dealer.score, WATCH_POSITION_TOP_RIGHT);
}
static void perform_stand(void) {
game_state = BJ_DEALER_PLAYING;
watch_display_text(WATCH_POSITION_BOTTOM, "Stnd");
display_score(player.score, WATCH_POSITION_SECONDS);
}
static void perform_hit(void) {
if (player.score == 21) {
perform_stand();
return; // Assume hitting on 21 is a mistake and stand
}
give_card(&player);
if (player.score > 21) {
game_state = BJ_BUST;
}
display_player_hand();
display_score(player.score, WATCH_POSITION_SECONDS);
}
static void dealer_performs_hits(void) {
give_card(&dealer);
display_dealer_hand();
if (dealer.score > 21) {
display_win();
} else if (dealer.score > player.score) {
display_lose();
} else {
display_dealer_hand();
display_score(dealer.score, WATCH_POSITION_TOP_RIGHT);
}
}
static void see_if_dealer_hits(void) {
if (dealer.score > 16) {
if (dealer.score > player.score) {
display_lose();
} else if (dealer.score < player.score) {
display_win();
} else {
display_tie();
}
} else {
dealer_performs_hits();
}
}
static void handle_button_presses(bool tap_control_on, bool hit) {
switch (game_state)
{
case BJ_TITLE_SCREEN:
if (!tap_turned_on && tap_control_on) {
if (movement_enable_tap_detection_if_available()) tap_turned_on = true;
}
begin_playing(tap_control_on);
break;
case BJ_PLAYING:
if (hit) {
perform_hit();
} else {
perform_stand();
}
break;
case BJ_DEALER_PLAYING:
see_if_dealer_hits();
break;
case BJ_BUST:
display_bust();
break;
case BJ_RESULT:
case BJ_WIN_RATIO:
display_title();
break;
}
}
static void toggle_tap_control(blackjack_face_state_t *state) {
if (state->tap_control_on) {
movement_disable_tap_detection_if_available();
state->tap_control_on = false;
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
} else {
bool tap_could_enable = movement_enable_tap_detection_if_available();
if (tap_could_enable) {
state->tap_control_on = true;
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
}
}
}
void blackjack_face_setup(uint8_t watch_face_index, void **context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(blackjack_face_state_t));
memset(*context_ptr, 0, sizeof(blackjack_face_state_t));
blackjack_face_state_t *state = (blackjack_face_state_t *)*context_ptr;
state->tap_control_on = false;
}
g_state = (blackjack_face_state_t *)*context_ptr;
}
void blackjack_face_activate(void *context) {
blackjack_face_state_t *state = (blackjack_face_state_t *) context;
(void) state;
display_title();
stack_deck();
}
bool blackjack_face_loop(movement_event_t event, void *context) {
blackjack_face_state_t *state = (blackjack_face_state_t *) context;
switch (event.event_type) {
case EVENT_ACTIVATE:
if (state->tap_control_on) watch_set_indicator(WATCH_INDICATOR_SIGNAL);
break;
case EVENT_TICK:
if (game_state == BJ_DEALER_PLAYING) {
see_if_dealer_hits();
}
else if (game_state == BJ_BUST) {
display_bust();
}
break;
case EVENT_LIGHT_BUTTON_UP:
case EVENT_DOUBLE_TAP:
handle_button_presses(state->tap_control_on, false);
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_ALARM_BUTTON_UP:
case EVENT_SINGLE_TAP:
handle_button_presses(state->tap_control_on, true);
break;
case EVENT_LIGHT_LONG_PRESS:
if (game_state == BJ_TITLE_SCREEN) {
display_win_ratio(state);
} else {
movement_illuminate_led();
}
break;
case EVENT_ALARM_LONG_PRESS:
if (game_state == BJ_TITLE_SCREEN) {
toggle_tap_control(state);
} else if (game_state == BJ_WIN_RATIO) {
// Reset the win-lose ratio
state->games_won = 0;
state->games_played = 0;
watch_display_text(WATCH_POSITION_BOTTOM, " 0Pct");
}
break;
case EVENT_TIMEOUT:
case EVENT_LOW_ENERGY_UPDATE:
if (tap_turned_on) {
movement_disable_tap_detection_if_available();
}
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void blackjack_face_resign(void *context) {
(void) context;
if (tap_turned_on) {
tap_turned_on = false;
movement_disable_tap_detection_if_available();
}
}
+96
View File
@@ -0,0 +1,96 @@
/*
* MIT License
*
* Copyright (c) 2023 Chris Ellis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef BLACKJACK_FACE_H_
#define BLACKJACK_FACE_H_
#include "movement.h"
/*
* Blackjack face
* ======================
*
* Simple blackjack game.
*
* Aces are 11 unless you'd but, and if so, they become 1.
* King, Queen, and jack are all 10 points.
* Dealer deals to themselves until they get at least 17.
* The game plays with one shuffled deck that gets reshuffled with every game.
*
* Press either ALARM or LIGHT to begin playing.
* Your score is in the Seconds position.
* The dealer's score is in the Top-Right position.
* The dealer's last-shown card is in the Top-Left position.
* Your cards are in the Bottom row. From left to right, they are oldest to newest. Up to four cards will be dislayed.
*
* To hit, press the ALARM button.
* To stand, press the LIGHT button.
* If you're at 21, you will stand if you try to hit, since we just assume it's a mispress on the button.
*
* Once you stand, the dealer will deal out to themselves once per second (or immidietly when you press the LIGHT or ALARM buttons).
* The game results are:
* WIN: You have a higher score than the dealer, but no more than 21. Or the dealer's score is over 21.
* LOSE: Your score is lower than the dealer's.
* BUST: Your score is above 21.
* TIE: Your score matches the dealer's final score
*
* On a watch that has the accelerometer, long-pressing the ALARM button on the Title Screen will turn on the ability to play by tapping.
* The SIGNAL indicator will display when tapping is enabled.
* Tapping once will behave like the ALARM button and hit.
* Tapping twice behave like the LIGHT button and stand. Warning: if you're using the LIS2DW board, it cannot register a double-tapping
* without seeing a single-tap first.
*
* Long-pressing the LIGHT button on the Title Screen will display your win rate as a percentage of games finished.
* It displays as games won / (games won + games lost) it does not include incomplete nor tied games.
* You can reset the win rate on that screen by long-pressing the ALARM button.
*
* | Cards | |
* |---------|--------------------------|
* | Value |2|3|4|5|6|7|8|9|10|J|Q|K|A|
* | Display |2|3|4|5|6|7|8|9| 0|-|=|≡|a|
* If you're using a custom display, Ace will display as 'A', not 'a'
*/
typedef struct {
bool tap_control_on;
uint16_t games_played;
uint16_t games_won;
} blackjack_face_state_t;
void blackjack_face_setup(uint8_t watch_face_index, void ** context_ptr);
void blackjack_face_activate(void *context);
bool blackjack_face_loop(movement_event_t event, void *context);
void blackjack_face_resign(void *context);
#define blackjack_face ((const watch_face_t){ \
blackjack_face_setup, \
blackjack_face_activate, \
blackjack_face_loop, \
blackjack_face_resign, \
NULL, \
})
#endif // blackjack_FACE_H_
+17 -81
View File
@@ -23,80 +23,6 @@
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* # Deadline Face
*
* This is a watch face for tracking deadlines. It draws inspiration from
* other watch faces of the project but focuses on keeping track of
* deadlines. You can enter and monitor up to four different deadlines by
* providing their respective date and time. The face has two modes:
* *running mode* and *settings mode*.
*
* ## Running Mode
*
* When the watch face is activated, it defaults to running mode. The top
* right corner shows the current deadline number, and the main display
* presents the time left until the deadline. The format of the display
* varies depending on the remaining time.
*
* - When less than a day is left, the display shows the remaining hours,
* minutes, and seconds in the form `HH:MM:SS`.
*
* - When less than a month is left, the display shows the remaining days
* and hours in the form `DD:HH` with the unit `dy` for days.
*
* - When less than a year is left, the display shows the remaining months
* and days in the form `MM:DD` with the unit `mo` for months.
*
* - When more than a year is left, the years and months are displayed in
* the form `YY:MM` with the unit `yr` for years.
*
* - When a deadline has passed in the last 24 hours, the display shows
* `over` to indicate that the deadline has just recently been reached.
*
* - When no deadline is set for a particular slot, or if a deadline has
* already passed by more than 24 hours, `--:--` is displayed.
*
* The user can navigate in running mode using the following buttons:
*
* - The *alarm button* moves the next deadline. There are currently four
* slots available for deadlines. When the last slot has been reached,
* pressing the button moves to the first slot.
*
* - A *long press* on the *alarm button* activates settings mode and
* enables configuring the currently selected deadline.
*
* - A *long press* on the *light button* activates a deadline alarm. The
* bell icon is displayed, and the alarm will ring upon reaching any of
* the deadlines set. It is important to note that the watch will not
* enter low-energy sleep mode while the alarm is enabled.
*
*
* ## Settings Mode
*
* In settings mode, the currently selected slot for a deadline can be
* configured by providing the date and the time. Like running mode, the
* top right corner of the display indicates the current deadline number.
* The main display shows the date and, on the next page, the time to be
* configured.
*
* The user can use the following buttons in settings mode.
*
* - The *light button* navigates through the different date and time
* settings, going from year, month, day, hour, to minute. The selected
* position is blinking.
*
* - A *long press* on the light button resets the date and time to the next
* day at midnight. This is the default deadline.
*
* - The *alarm button* increments the currently selected position. A *long
* press* on the *alarm button* changes the value faster.
*
* - The *mode button* exists setting mode and returns to *running mode*.
* Here the selected deadline slot can be changed.
*
*/
#include <stdlib.h>
#include <string.h>
#include "deadline_face.h"
@@ -156,26 +82,36 @@ static inline int _days_in_month(int16_t month, int16_t year)
/* Play beep sound based on type */
static inline void _beep(beep_type_t beep_type)
{
static int8_t beep_sequence[] = {
0, 4,
0, 6,
0, 6,
0
};
if (!movement_button_should_sound())
return;
switch (beep_type) {
case BEEP_BUTTON:
watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
beep_sequence[0] = BUZZER_NOTE_C7;
beep_sequence[2] = 0;
break;
case BEEP_ENABLE:
watch_buzzer_play_note(BUZZER_NOTE_G7, 50);
watch_buzzer_play_note(BUZZER_NOTE_REST, 75);
watch_buzzer_play_note(BUZZER_NOTE_C8, 75);
beep_sequence[0] = BUZZER_NOTE_G7;
beep_sequence[2] = BUZZER_NOTE_REST;
beep_sequence[4] = BUZZER_NOTE_C8;
break;
case BEEP_DISABLE:
watch_buzzer_play_note(BUZZER_NOTE_C8, 50);
watch_buzzer_play_note(BUZZER_NOTE_REST, 75);
watch_buzzer_play_note(BUZZER_NOTE_G7, 75);
beep_sequence[0] = BUZZER_NOTE_C8;
beep_sequence[2] = BUZZER_NOTE_REST;
beep_sequence[4] = BUZZER_NOTE_G7;
break;
}
movement_play_sequence(beep_sequence, 0);
}
/* Change tick frequency */
+74
View File
@@ -26,6 +26,80 @@
#ifndef DEADLINE_FACE_H_
#define DEADLINE_FACE_H_
/*
* # Deadline Face
*
* This is a watch face for tracking deadlines. It draws inspiration from
* other watch faces of the project but focuses on keeping track of
* deadlines. You can enter and monitor up to four different deadlines by
* providing their respective date and time. The face has two modes:
* *running mode* and *settings mode*.
*
* ## Running Mode
*
* When the watch face is activated, it defaults to running mode. The top
* right corner shows the current deadline number, and the main display
* presents the time left until the deadline. The format of the display
* varies depending on the remaining time.
*
* - When less than a day is left, the display shows the remaining hours,
* minutes, and seconds in the form `HH:MM:SS`.
*
* - When less than a month is left, the display shows the remaining days
* and hours in the form `DD:HH` with the unit `dy` for days.
*
* - When less than a year is left, the display shows the remaining months
* and days in the form `MM:DD` with the unit `mo` for months.
*
* - When more than a year is left, the years and months are displayed in
* the form `YY:MM` with the unit `yr` for years.
*
* - When a deadline has passed in the last 24 hours, the display shows
* `over` to indicate that the deadline has just recently been reached.
*
* - When no deadline is set for a particular slot, or if a deadline has
* already passed by more than 24 hours, `--:--` is displayed.
*
* The user can navigate in running mode using the following buttons:
*
* - The *alarm button* moves the next deadline. There are currently four
* slots available for deadlines. When the last slot has been reached,
* pressing the button moves to the first slot.
*
* - A *long press* on the *alarm button* activates settings mode and
* enables configuring the currently selected deadline.
*
* - A *long press* on the *light button* activates a deadline alarm. The
* bell icon is displayed, and the alarm will ring upon reaching any of
* the deadlines set. It is important to note that the watch will not
* enter low-energy sleep mode while the alarm is enabled.
*
*
* ## Settings Mode
*
* In settings mode, the currently selected slot for a deadline can be
* configured by providing the date and the time. Like running mode, the
* top right corner of the display indicates the current deadline number.
* The main display shows the date and, on the next page, the time to be
* configured.
*
* The user can use the following buttons in settings mode.
*
* - The *light button* navigates through the different date and time
* settings, going from year, month, day, hour, to minute. The selected
* position is blinking.
*
* - A *long press* on the light button resets the date and time to the next
* day at midnight. This is the default deadline.
*
* - The *alarm button* increments the currently selected position. A *long
* press* on the *alarm button* changes the value faster.
*
* - The *mode button* exists setting mode and returns to *running mode*.
* Here the selected deadline slot can be changed.
*
*/
#include "movement.h"
/* Modes of face */
@@ -0,0 +1,667 @@
/*
* 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"
#include "delay.h"
typedef enum {
JUMPING_FINAL_FRAME = 0,
NOT_JUMPING,
JUMPING_START,
} RunnerJumpState;
typedef enum {
SCREEN_TITLE = 0,
SCREEN_SCORE,
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;
// always-on, left, right, bottom, jump-top, jump-left, jump-right
int8_t classic_ball_arr_com[] = {1, 0, 1, 0, 2, 1, 2};
int8_t classic_ball_arr_seg[] = {20, 20, 21, 21, 20, 17, 21};
int8_t custom_ball_arr_com[] = {2, 1, 1, 0, 3, 3, 2};
int8_t custom_ball_arr_seg[] = {15, 15, 14, 15, 14, 15, 14};
// obstacle 0-11
int8_t classic_obstacle_arr_com[] = {0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1};
int8_t classic_obstacle_arr_seg[] = {18, 19, 20, 21, 22, 23, 0, 1, 2, 4, 5, 6};
int8_t custom_obstacle_arr_com[] = {1, 1, 1, 1, 1, 0, 1, 0, 3, 0, 0, 2};
int8_t custom_obstacle_arr_seg[] = {22, 16, 15, 14, 1, 2, 3, 4, 4, 5, 6, 7};
int8_t *ball_arr_com;
int8_t *ball_arr_seg;
int8_t *obstacle_arr_com;
int8_t *obstacle_arr_seg;
static game_state_t game_state;
static const uint8_t _num_bits_obst_pattern = sizeof(game_state.obst_pattern) * 8;
int8_t start_tune[] = {
BUZZER_NOTE_C5, 15,
BUZZER_NOTE_E5, 15,
BUZZER_NOTE_G5, 15,
0
};
int8_t lose_tune[] = {
BUZZER_NOTE_D3, 10,
BUZZER_NOTE_C3SHARP_D3FLAT, 10,
BUZZER_NOTE_C3, 10,
0
};
static void print_binary(uint32_t value, int bits) {
#if __EMSCRIPTEN__
for (int i = bits - 1; i >= 0; i--) {
// Print each bit
printf("%u", (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(ball_arr_com[3], ball_arr_seg[3]);
watch_set_pixel(ball_arr_com[2], ball_arr_seg[2]);
watch_set_pixel(ball_arr_com[1], ball_arr_seg[1]);
watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]);
watch_clear_pixel(ball_arr_com[6], ball_arr_seg[6]);
watch_clear_pixel(ball_arr_com[5], ball_arr_seg[5]);
watch_clear_pixel(ball_arr_com[4], ball_arr_seg[4]);
}
else {
watch_clear_pixel(ball_arr_com[3], ball_arr_seg[3]);
watch_clear_pixel(ball_arr_com[2], ball_arr_seg[2]);
watch_clear_pixel(ball_arr_com[1], ball_arr_seg[1]);
watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]);
watch_set_pixel(ball_arr_com[6], ball_arr_seg[6]);
watch_set_pixel(ball_arr_com[5], ball_arr_seg[5]);
watch_set_pixel(ball_arr_com[4], ball_arr_seg[4]);
}
}
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_text(WATCH_POSITION_TOP_LEFT, buf);
}
else {
score %= (MAX_DISP_SCORE + 1);
sprintf(buf, "%2d", score);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
}
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_text(WATCH_POSITION_TOP_RIGHT, " "); // Blink the 0 fuel to show it cannot be refilled.
return;
}
sprintf(buf, "%2d", game_state.fuel);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
static void check_and_reset_hi_score(endless_runner_state_t *state) {
// Resets the hi score at the beginning of each month.
watch_date_time_t date_time = movement_get_local_date_time();
if ((state -> year_last_hi_score != date_time.unit.year) ||
(state -> month_last_hi_score != date_time.unit.month))
{
// The high score resets itself every new month.
state -> hi_score = 0;
state -> year_last_hi_score = date_time.unit.year;
state -> month_last_hi_score = date_time.unit.month;
}
}
static void display_difficulty(uint16_t difficulty) {
static const char *labels[] = {
[DIFF_BABY] = " b",
[DIFF_EASY] = " E",
[DIFF_HARD] = " H",
[DIFF_FUEL] = " F",
[DIFF_FUEL_1] = "1F",
[DIFF_NORM] = " N"
};
watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]);
game_state.fuel_mode = difficulty >= DIFF_FUEL && difficulty <= DIFF_FUEL_1;
}
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 display_sound_indicator(bool soundOn) {
if (soundOn){
watch_set_indicator(WATCH_INDICATOR_BELL);
} else {
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
}
static void toggle_sound(endless_runner_state_t *state) {
state -> soundOn = !state -> soundOn;
display_sound_indicator(state -> soundOn);
if (state -> soundOn){
watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
}
}
static void enable_tap_control(endless_runner_state_t *state) {
if (!state->tap_control_on) {
movement_enable_tap_detection_if_available();
state->tap_control_on = true;
}
}
static void disable_tap_control(endless_runner_state_t *state) {
if (state->tap_control_on) {
movement_disable_tap_detection_if_available();
state->tap_control_on = false;
}
}
static void display_title(endless_runner_state_t *state) {
game_state.curr_screen = SCREEN_TITLE;
watch_clear_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP, "ENdLS", "ER ");
watch_display_text(WATCH_POSITION_BOTTOM, "RUNNER");
display_sound_indicator(state -> soundOn);
}
static void display_score_screen(endless_runner_state_t *state) {
uint16_t hi_score = state -> hi_score;
uint8_t difficulty = state -> difficulty;
bool sound_on = state -> soundOn;
memset(&game_state, 0, sizeof(game_state));
game_state.curr_screen = SCREEN_SCORE;
game_state.sec_before_moves = 1; // The first obstacles will all be 0s, which is about an extra second of delay.
if (sound_on) game_state.sec_before_moves--; // Start chime is about 1 second
watch_set_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN ", "ER ");
if (hi_score > MAX_HI_SCORE) {
watch_display_text(WATCH_POSITION_BOTTOM, "HS --");
}
else {
char buf[10];
sprintf(buf, "HS%4d", hi_score);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
display_difficulty(difficulty);
display_sound_indicator(sound_on);
}
static void display_time(void) {
static watch_date_time_t previous_date_time;
watch_date_time_t date_time = movement_get_local_date_time();
movement_clock_mode_t clock_mode_24h = movement_clock_mode_24h();
char buf[6 + 1];
// If the hour needs updating or it's the first time displaying the time
if ((game_state.curr_screen != SCREEN_TIME) || (date_time.unit.hour != previous_date_time.unit.hour)) {
uint8_t hour = date_time.unit.hour;
game_state.curr_screen = SCREEN_TIME;
if (!watch_sleep_animation_is_running()) {
watch_set_colon();
watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500);
}
if (clock_mode_24h != MOVEMENT_CLOCK_MODE_12H) watch_set_indicator(WATCH_INDICATOR_24H);
else {
if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM);
hour %= 12;
if (hour == 0) hour = 12;
}
sprintf( buf, clock_mode_24h == MOVEMENT_CLOCK_MODE_024H ? "%02d%02d " : "%2d%02d ", hour, date_time.unit.minute);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
// If only the minute need updating
else {
sprintf( buf, "%02d", date_time.unit.minute);
watch_display_text(WATCH_POSITION_MINUTES, buf);
}
previous_date_time.reg = date_time.reg;
}
static void begin_playing(endless_runner_state_t *state) {
uint8_t difficulty = state -> difficulty;
game_state.curr_screen = SCREEN_PLAYING;
watch_clear_colon();
display_sound_indicator(state -> soundOn);
movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ);
if (game_state.fuel_mode) {
watch_clear_display();
game_state.obst_pattern = get_random_fuel(0);
if ((16 * JUMP_FRAMES_FUEL_RECHARGE) < JUMP_FRAMES_FUEL) // 16 frames of zeros at the start of a level
game_state.fuel = JUMP_FRAMES_FUEL - (16 * JUMP_FRAMES_FUEL_RECHARGE); // Have it below its max to show it counting up when starting.
if (game_state.fuel < JUMP_FRAMES_FUEL_RECHARGE) game_state.fuel = JUMP_FRAMES_FUEL_RECHARGE;
}
else {
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, " ");
game_state.obst_pattern = get_random_legal(0, difficulty);
}
game_state.jump_state = NOT_JUMPING;
display_ball(game_state.jump_state != NOT_JUMPING);
display_score( game_state.curr_score);
if (state -> soundOn){
watch_buzzer_play_sequence(start_tune, NULL);
}
}
static void display_lose_screen(endless_runner_state_t *state) {
game_state.curr_screen = SCREEN_LOSE;
game_state.curr_score = 0;
watch_clear_display();
watch_display_text(WATCH_POSITION_BOTTOM, " LOSE ");
if (state -> soundOn) {
watch_buzzer_play_sequence(lose_tune, NULL);
delay_ms(600);
}
}
static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t *state) {
static bool prev_obst_pos_two = 0;
switch (grid_loc)
{
case 2:
game_state.loc_2_on = obstacle;
if (obstacle)
watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
else if (game_state.jump_state != NOT_JUMPING) {
watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
if (game_state.fuel_mode && prev_obst_pos_two)
add_to_score(state);
}
prev_obst_pos_two = obstacle;
break;
case 3:
game_state.loc_3_on = obstacle;
if (obstacle)
watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
else if (game_state.jump_state != NOT_JUMPING)
watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
break;
case 1:
if (!game_state.fuel_mode && obstacle) // If an obstacle is here, it means the ball cleared it
add_to_score(state);
//fall through
default:
if (obstacle)
watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
else
watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
break;
}
}
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;
state->tap_control_on = false;
}
}
void endless_runner_face_activate(void *context) {
(void) context;
bool is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM;
ball_arr_com = is_custom_lcd ? custom_ball_arr_com : classic_ball_arr_com;
ball_arr_seg = is_custom_lcd ? custom_ball_arr_seg : classic_ball_arr_seg;
obstacle_arr_com = is_custom_lcd ? custom_obstacle_arr_com : classic_obstacle_arr_com;
obstacle_arr_seg = is_custom_lcd ? custom_obstacle_arr_seg : classic_obstacle_arr_seg;
if (watch_sleep_animation_is_running()) {
watch_stop_blink();
}
}
bool endless_runner_face_loop(movement_event_t event, void *context) {
endless_runner_state_t *state = (endless_runner_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
disable_tap_control(state);
check_and_reset_hi_score(state);
display_title(state);
break;
case EVENT_TICK:
switch (game_state.curr_screen)
{
case SCREEN_TITLE:
case SCREEN_SCORE:
case SCREEN_LOSE:
case SCREEN_TIME:
break;
default:
update_game(state, event.subsecond);
break;
}
break;
case EVENT_LIGHT_BUTTON_UP:
case EVENT_ALARM_BUTTON_UP:
switch (game_state.curr_screen) {
case SCREEN_SCORE:
enable_tap_control(state);
begin_playing(state);
break;
case SCREEN_TITLE:
enable_tap_control(state);
// fall through
case SCREEN_TIME:
case SCREEN_LOSE:
watch_clear_display();
display_score_screen(state);
}
break;
case EVENT_LIGHT_LONG_PRESS:
if (game_state.curr_screen == SCREEN_SCORE)
change_difficulty(state);
break;
case EVENT_SINGLE_TAP:
case EVENT_DOUBLE_TAP:
if (state->difficulty > DIFF_HARD) break; // Don't do this on fuel modes
// Allow starting a new game by tapping.
if (game_state.curr_screen == SCREEN_SCORE) {
begin_playing(state);
break;
}
else if (game_state.curr_screen == SCREEN_LOSE) {
display_score_screen(state);
break;
}
//fall through
case EVENT_LIGHT_BUTTON_DOWN:
case EVENT_ALARM_BUTTON_DOWN:
if (game_state.curr_screen == SCREEN_PLAYING && game_state.jump_state == NOT_JUMPING){
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_TITLE || game_state.curr_screen == SCREEN_SCORE)
toggle_sound(state);
break;
case EVENT_TIMEOUT:
disable_tap_control(state);
if (game_state.curr_screen != SCREEN_SCORE)
display_score_screen(state);
break;
case EVENT_LOW_ENERGY_UPDATE:
if (game_state.curr_screen != SCREEN_TIME) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN ", "ER ");
display_sound_indicator(state -> soundOn);
display_difficulty(state->difficulty);
}
display_time();
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void endless_runner_face_resign(void *context) {
endless_runner_state_t *state = (endless_runner_state_t *)context;
disable_tap_control(state);
}
@@ -0,0 +1,65 @@
/*
* 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.
If the accelerometer is installed, you can tap the screen to jump and move through the menus after using the
buttons to go into the first game.
High-score is displayed on the top-right on the title screen. During a game, the current score is displayed.
*/
typedef struct {
uint16_t hi_score : 10;
uint8_t difficulty : 3;
uint8_t month_last_hi_score : 4;
uint8_t year_last_hi_score : 6;
uint8_t soundOn : 1;
uint8_t tap_control_on : 1;
uint8_t unused : 7;
} 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_
+244 -227
View File
@@ -2,6 +2,7 @@
* MIT License
*
* Copyright (c) 2022 Andreas Nebinger
* Copyright (c) 2025 Alessandro Genova
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -24,11 +25,13 @@
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include "fast_stopwatch_face.h"
#include "watch.h"
#include "watch_common_display.h"
#include "watch_utility.h"
#include "watch_rtc.h"
#include "slcd.h"
/*
This watch face implements the original F-91W stopwatch functionality
@@ -40,173 +43,247 @@
turns on on each button press or it doesn't.
*/
#if __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#else
#include "tc.h"
#endif
// distant future for background task: January 1, 2083
static const watch_date_time_t distant_future = {
.unit = {0, 0, 0, 1, 1, 63}
};
static uint32_t _ticks;
static uint32_t _lap_ticks;
static uint8_t _blink_ticks;
static uint32_t _old_seconds;
static uint8_t _old_minutes;
static uint8_t _hours;
static bool _colon;
static bool _is_running;
#if __EMSCRIPTEN__
static long _em_interval_id = 0;
void em_cb_handler(void *userData) {
// interrupt handler for emscripten 128 Hz callbacks
(void) userData;
_ticks++;
}
static void _cb_initialize() { }
static inline void _cb_stop() {
emscripten_clear_interval(_em_interval_id);
_em_interval_id = 0;
_is_running = false;
}
static inline void _cb_start() {
// initiate 128 hz callback
_em_interval_id = emscripten_set_interval(em_cb_handler, (double)(1000/128), (void *)NULL);
}
#else
static inline void _cb_start() {
// start the TC1 timer
tc_enable(1);
_is_running = true;
}
static inline void _cb_stop() {
// stop the TC1 timer
tc_disable(1);
_is_running = false;
}
static void _cb_initialize() {
tc_init(1, GENERIC_CLOCK_3, TC_PRESCALER_DIV4);
tc_set_counter_mode(1, TC_COUNTER_MODE_8BIT);
tc_set_run_in_standby(1, true);
_cb_stop();
tc_count8_set_period(1, 1); // 1024 Hz divided by 4 divided by 2 results in a 128 Hz interrupt
/// FIXME: #SecondMovement, we need a gossamer wrapper for interrupts.
TC1->COUNT8.INTENSET.bit.OVF = 1;
NVIC_ClearPendingIRQ(TC1_IRQn);
NVIC_EnableIRQ (TC1_IRQn);
}
void irq_handler_tc1(void);
void irq_handler_tc1(void) {
// interrupt handler for TC1 (globally!)
_ticks++;
TC1->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
}
#endif
// Loosely implement the watch as a state machine
typedef enum {
SW_STATUS_IDLE = 0,
SW_STATUS_RUNNING,
SW_STATUS_RUNNING_LAPPING,
SW_STATUS_STOPPED,
SW_STATUS_STOPPED_LAPPING
} stopwatch_status_t;
static inline void _button_beep() {
// play a beep as confirmation for a button press (if applicable)
if (movement_button_should_sound()) watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume());
}
// How quickly should the elapsing time be displayed?
// This is just for looks, timekeeping is always accurate to 128Hz
static const uint8_t DISPLAY_RUNNING_RATE = 32;
static const uint8_t DISPLAY_RUNNING_RATE_SLOW = 2;
/// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter
/// on the lcd.
/// @param ticks
static void _display_ticks(uint32_t ticks) {
char buf[14];
uint8_t sec_100 = (ticks & 0x7F) * 100 / 128;
static void _display_elapsed(fast_stopwatch_state_t *state, uint32_t ticks) {
char buf[3];
if (state->slow_refresh && (state->status == SW_STATUS_RUNNING || state->status == SW_STATUS_IDLE)) {
watch_display_character_lp_seconds(' ', 8);
watch_display_character_lp_seconds(' ', 9);
} else {
uint8_t sec_100 = (ticks & 0x7F) * 100 / 128;
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
}
uint32_t seconds = ticks >> 7;
if (seconds == state->old_display.seconds) {
return;
}
state->old_display.seconds = seconds;
sprintf(buf, "%02lu", seconds % 60);
watch_display_text(WATCH_POSITION_MINUTES, buf);
uint32_t minutes = seconds / 60;
if (_hours) {
sprintf(buf, "%2u", _hours);
if (minutes == state->old_display.minutes) {
return;
}
state->old_display.minutes = minutes;
sprintf(buf, "%02lu", minutes % 60);
watch_display_text(WATCH_POSITION_HOURS, buf);
uint32_t hours = (minutes / 60) % 24;
if (hours == state->old_display.hours) {
return;
}
state->old_display.hours = hours;
if (hours) {
sprintf(buf, "%2lu", hours);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
} else {
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
}
sprintf(buf, "%02lu%02lu%02u", minutes, (seconds % 60), sec_100);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
/// @brief Displays the current stopwatch time on the LCD (more optimized than _display_ticks())
static void _draw() {
if (_lap_ticks == 0) {
char buf[14];
uint8_t sec_100 = (_ticks & 0x7F) * 100 / 128;
if (_is_running) {
uint32_t seconds = _ticks >> 7;
if (seconds != _old_seconds) {
// seconds have changed
_old_seconds = seconds;
uint8_t minutes = seconds / 60;
seconds %= 60;
if (minutes != _old_minutes) {
// minutes have changed, draw everything
_old_minutes = minutes;
minutes %= 60;
if (_hours) {
// with hour indicator
sprintf(buf, "%2u", _hours);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
} else {
// no hour indicator
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
}
sprintf(buf, "%02u%02lu%02u", minutes, seconds, sec_100);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
} else {
// just draw seconds
sprintf(buf, "%02lu", seconds);
// note that we're drawing the seconds in the "minutes" position, since this
// watch face uses the "seconds" position for hundredths of seconds
watch_display_text(WATCH_POSITION_MINUTES, buf);
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
}
static void _draw_indicators(fast_stopwatch_state_t *state, movement_event_t event, uint32_t elapsed) {
uint8_t subsecond;
bool tock;
switch (state->status) {
case SW_STATUS_RUNNING:
subsecond = elapsed & 127;
tock = subsecond >= 64;
watch_clear_indicator(WATCH_INDICATOR_LAP);
if (tock) {
watch_clear_colon();
} else {
// only draw 100ths of seconds
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
watch_set_colon();
}
} else {
_display_ticks(_ticks);
}
}
if (_is_running) {
// blink the colon every half second
uint8_t blink_ticks = ((_ticks >> 6) & 1);
if (blink_ticks != _blink_ticks) {
_blink_ticks = blink_ticks;
_colon = !_colon;
if (_colon) watch_set_colon();
else watch_clear_colon();
}
return;
case SW_STATUS_RUNNING_LAPPING:
tock = event.subsecond > 0;
if (tock) {
watch_clear_indicator(WATCH_INDICATOR_LAP);
watch_clear_colon();
} else {
watch_set_indicator(WATCH_INDICATOR_LAP);
watch_set_colon();
}
return;
case SW_STATUS_STOPPED_LAPPING:
watch_set_indicator(WATCH_INDICATOR_LAP);
watch_set_colon();
return;
case SW_STATUS_STOPPED:
case SW_STATUS_IDLE:
default:
watch_clear_indicator(WATCH_INDICATOR_LAP);
watch_set_colon();
return;
}
}
static inline void _update_lap_indicator() {
if (_lap_ticks) watch_set_indicator(WATCH_INDICATOR_LAP);
else watch_clear_indicator(WATCH_INDICATOR_LAP);
static uint8_t get_refresh_rate(fast_stopwatch_state_t *state) {
switch (state->status) {
case SW_STATUS_RUNNING:
if (state->slow_refresh) {
return DISPLAY_RUNNING_RATE_SLOW;
} else {
return DISPLAY_RUNNING_RATE;
}
case SW_STATUS_RUNNING_LAPPING:
return 2;
case SW_STATUS_STOPPED:
case SW_STATUS_IDLE:
default:
return 1;
}
}
static inline void _set_colon() {
watch_set_colon();
_colon = true;
static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counter, movement_event_type_t event_type) {
switch (state->status) {
case SW_STATUS_IDLE:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING;
state->start_counter = counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
case EVENT_LIGHT_LONG_PRESS:
state->slow_refresh = !state->slow_refresh;
return;
default:
return;
}
case SW_STATUS_RUNNING:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_STOPPED;
state->stop_counter = counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING_LAPPING;
state->lap_counter = counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
default:
return;
}
case SW_STATUS_RUNNING_LAPPING:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_STOPPED_LAPPING;
state->stop_counter = counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING;
state->lap_counter = counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
case EVENT_LIGHT_LONG_PRESS:
state->status = SW_STATUS_RUNNING;
state->slow_refresh = !state->slow_refresh;
movement_request_tick_frequency(get_refresh_rate(state));
return;
default:
return;
}
case SW_STATUS_STOPPED_LAPPING:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING_LAPPING;
state->start_counter = counter - state->stop_counter + state->start_counter;
state->lap_counter = counter - state->stop_counter + state->lap_counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = SW_STATUS_STOPPED;
return;
default:
return;
}
case SW_STATUS_STOPPED:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING;
state->start_counter = counter - state->stop_counter + state->start_counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = SW_STATUS_IDLE;
return;
default:
return;
}
default:
return;
}
}
static uint32_t elapsed_time(fast_stopwatch_state_t *state, rtc_counter_t counter) {
switch (state->status) {
case SW_STATUS_IDLE:
return 0;
case SW_STATUS_RUNNING:
return counter - state->start_counter;
case SW_STATUS_RUNNING_LAPPING:
case SW_STATUS_STOPPED_LAPPING:
return state->lap_counter - state->start_counter;
case SW_STATUS_STOPPED:
return state->stop_counter - state->start_counter;
default:
return 0;
}
}
void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
@@ -215,114 +292,54 @@ void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
*context_ptr = malloc(sizeof(fast_stopwatch_state_t));
memset(*context_ptr, 0, sizeof(fast_stopwatch_state_t));
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)*context_ptr;
_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0;
_is_running = _colon = false;
state->light_on_button = true;
}
if (!_is_running) {
// prepare the 128 Hz callback source
_cb_initialize();
state->start_counter = 0;
state->stop_counter = 0;
state->lap_counter = 0;
state->status = SW_STATUS_IDLE;
}
}
void fast_stopwatch_face_activate(void *context) {
(void) context;
if (_is_running) {
// The background task will keep the watch from entering low energy mode while the stopwatch is on screen.
movement_schedule_background_task(distant_future);
}
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *) context;
// force full re-draw
state->old_display.seconds = UINT_MAX;
state->old_display.minutes = UINT_MAX;
state->old_display.hours = UINT_MAX;
movement_request_tick_frequency(get_refresh_rate(state));
}
bool fast_stopwatch_face_loop(movement_event_t event, void *context) {
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)context;
// handle overflow of fast ticks
while (_ticks >= (128 * 60 * 60)) {
_ticks -= (128 * 60 * 60);
_hours++;
if (_hours >= 24) _hours -= 24;
// initiate a re-draw
_old_minutes = 59;
}
rtc_counter_t counter = watch_rtc_get_counter();
state_transition(state, counter, event.event_type);
rtc_counter_t elapsed = elapsed_time(state, counter);
switch (event.event_type) {
case EVENT_ACTIVATE:
_set_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST");
_update_lap_indicator();
if (_is_running) movement_request_tick_frequency(16);
_display_ticks(_lap_ticks ? _lap_ticks : _ticks);
break;
case EVENT_TICK:
_draw();
break;
case EVENT_LIGHT_LONG_PRESS:
// kind od hidden feature: long press toggles light on or off
state->light_on_button = !state->light_on_button;
if (state->light_on_button) movement_illuminate_led();
else watch_set_led_off();
_draw_indicators(state, event, elapsed);
_display_elapsed(state, elapsed);
break;
case EVENT_ALARM_BUTTON_DOWN:
_is_running = !_is_running;
if (_is_running) {
// start or continue stopwatch
movement_request_tick_frequency(16);
// register 128 hz callback for time measuring
_cb_start();
// schedule the keepalive task when running
movement_schedule_background_task(distant_future);
} else {
// stop the stopwatch
_cb_stop();
movement_request_tick_frequency(1);
_set_colon();
// cancel the keepalive task
movement_cancel_background_task();
}
_draw();
_button_beep();
break;
case EVENT_LIGHT_BUTTON_DOWN:
if (state->light_on_button) movement_illuminate_led();
if (_is_running) {
if (_lap_ticks) {
// clear lap and continue running
_lap_ticks = 0;
movement_request_tick_frequency(16);
} else {
// set lap ticks and stop updating the display
_lap_ticks = _ticks;
movement_request_tick_frequency(2);
_set_colon();
}
} else {
if (_lap_ticks) {
// clear lap and show running stopwatch
_lap_ticks = 0;
} else if (_ticks) {
// reset stopwatch
_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0;
_button_beep();
}
}
_display_ticks(_ticks);
_update_lap_indicator();
break;
case EVENT_TIMEOUT:
if (!_is_running) movement_move_to_face(0);
break;
case EVENT_LOW_ENERGY_UPDATE:
_draw();
case EVENT_LIGHT_LONG_PRESS:
_button_beep();
// fall through
case EVENT_TICK:
_draw_indicators(state, event, elapsed);
_display_elapsed(state, elapsed);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void fast_stopwatch_face_resign(void *context) {
(void) context;
// cancel the keepalive task
movement_cancel_background_task();
movement_request_tick_frequency(1);
}
+10 -7
View File
@@ -55,7 +55,16 @@
#include "movement.h"
typedef struct {
bool light_on_button; // determines whether the light button actually triggers the led
rtc_counter_t start_counter; // rtc counter when the stopwatch was started
rtc_counter_t lap_counter; // rtc counter when the stopwatch was lapped
rtc_counter_t stop_counter; // rtc counter when the stopwatch was stopped
uint8_t status; // the status the stopwatch is in (idle, running, stopped)
bool slow_refresh; // update the display slowly (same 128Hz timekeeping accuracy)
struct {
rtc_counter_t seconds;
rtc_counter_t minutes;
rtc_counter_t hours;
} old_display; // the digits currently being displayed on screen
} fast_stopwatch_state_t;
void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr);
@@ -63,12 +72,6 @@ void fast_stopwatch_face_activate(void *context);
bool fast_stopwatch_face_loop(movement_event_t event, void *context);
void fast_stopwatch_face_resign(void *context);
#if __EMSCRIPTEN__
void em_cb_handler(void *userData);
#else
void TC2_Handler(void);
#endif
#define fast_stopwatch_face ((const watch_face_t){ \
fast_stopwatch_face_setup, \
fast_stopwatch_face_activate, \
+407
View File
@@ -0,0 +1,407 @@
/*
* 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_common_display.h"
#define KING 12
#define QUEEN 11
#define JACK 10
#define TITLE_TEXT "Hi-Lo"
#define GAME_BOARD_SIZE 6
#define MAX_BOARDS 40
#define GUESSES_PER_SCREEN 5
#define WIN_SCORE (MAX_BOARDS * GUESSES_PER_SCREEN)
#define BOARD_DISPLAY_START 4
#define BOARD_DISPLAY_END 9
#define MIN_CARD_VALUE 2
#define MAX_CARD_VALUE KING
#define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1)
#define CARD_SUIT_COUNT 4
#define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT)
#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;
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_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
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) {
digit_mapping_t segmap;
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) {
segmap = Custom_LCD_Display_Mapping[position];
} else {
segmap = Classic_LCD_Display_Mapping[position];
}
const uint8_t com_pin = segmap.segment[segment].address.com;
const uint8_t seg = segmap.segment[segment].address.seg;
watch_set_pixel(com_pin, seg);
}
static inline size_t get_display_position(size_t board_position) {
return FLIP_BOARD_DIRECTION ? BOARD_DISPLAY_START + board_position : BOARD_DISPLAY_END - board_position;
}
static void render_board_position(size_t board_position) {
const size_t display_position = get_display_position(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 KING: // K (≡)
watch_display_character(' ', display_position);
set_segment_at_position(A, display_position);
set_segment_at_position(D, display_position);
set_segment_at_position(G, display_position);
break;
case QUEEN: // Q (=)
watch_display_character(' ', display_position);
set_segment_at_position(A, display_position);
set_segment_at_position(D, display_position);
break;
case JACK: // J (-)
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_text(WATCH_POSITION_TOP_RIGHT, buf);
}
static void render_final_score(void) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "SCORE", "SC ");
char buf[7] = {0};
const uint8_t complete_boards = score / GUESSES_PER_SCREEN;
snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score);
watch_set_colon();
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
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_text(WATCH_POSITION_TOP_LEFT, "==");
break;
case HL_GUESS_HIGHER:
watch_display_text(WATCH_POSITION_TOP_LEFT, "HI");
break;
case HL_GUESS_LOWER:
watch_display_text(WATCH_POSITION_TOP_LEFT, "LO");
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_text_with_fallback(WATCH_POSITION_TOP_LEFT, "End", "GO");
game_board[guess_position].revealed = true;
watch_display_text(WATCH_POSITION_BOTTOM, "------");
render_board_position(guess_position - 1);
render_board_position(guess_position);
if (game_board[guess_position].value == JACK && guess_position < GAME_BOARD_SIZE) // Adds a space in case the revealed option is '-'
watch_display_character(' ', get_display_position(guess_position + 1));
game_state = HL_GS_LOSE;
return;
}
if (score >= WIN_SCORE) {
// Win, perhaps some kind of animation sequence?
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "WI");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WINNER", "winnEr");
game_state = HL_GS_WIN;
return;
}
// 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_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
game_state = HL_GS_TITLE_SCREEN;
break;
default:
watch_display_text(WATCH_POSITION_BOTTOM, "ERROR");
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;
stack_deck();
}
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_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
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.
}
+106
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_
+1 -2
View File
@@ -96,8 +96,7 @@ static inline void _inc_uint8(uint8_t *value, uint8_t step, uint8_t max) {
static uint32_t _get_now_ts() {
// returns the current date time as unix timestamp
watch_date_time_t now = watch_rtc_get_date_time();
return watch_utility_date_time_to_unix_time(now, 0);
return movement_get_utc_timestamp();
}
static inline void _button_beep() {
+577
View File
@@ -0,0 +1,577 @@
/*
* MIT License
*
* Copyright (c) 2024 Klingon Jane
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Emulator only: need time() to seed the random number generator.
#if __EMSCRIPTEN__
#include <time.h>
#endif
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "lander_face.h"
#include "watch_common_display.h"
#ifndef max
#define max(x, y) ((y) > (x) ? (y) : (x))
#endif
#ifndef min
#define min(x, y) ((x) > (y) ? (y) : (x))
#endif
#define LANDER_TICK_FREQUENCY 8
#define MONSTER_DISPLAY_TICKS 9
#define ENGINE_THRUST 11
#define MODE_WAITING_TO_START 0
#define MODE_DISPLAY_SKILL_LEVEL 1
#define MODE_PLAYING 2
#define MODE_TOUCHDOWN_BLANK 3
#define MODE_DISPLAY_FINAL_STATUS 4
#define MODE_MONSTER 5
#define MODE_FIND_EARTH_MESSAGE 6
#define CREWS_COMPLIMENT 13
// Granularity is divisions per foot - height display
#define GRANUL 40
// Next lines for repeat heroes only.
#define PROMOTION_INTERVAL 3
#define LEVEL_ACE 8
#define LEVEL_STARBUCK 11
#define HARD_EARTH_INCREMENTS 11
#define MAX_HARD_EARTH_CHANCE 6
// The gory final result calculations:
#define SPEED_FATALITY_ALL 41
#define SPEED_FATALITY_NONE 26
#define SPEED_NO_DAMAGE 21
#define SPEED_LEVEL_INCREMENTS 2
#define SPEED_MAJOR_CRASH 73
#define MAJOR_CRASH_INCREMENTS 65
#define SPEED_INJURY_NONE 20
#define SPEED_INJURY_FULCRUM 32
#define INJURY_FULCRUM_PROB 65
#define FUEL_SCORE_GOOD 145
#define FUEL_SCORE_GREAT 131
#define FUEL_SCORE_FANTASTIC 125
// Joey Castillo to oversee storage allocation row
#define LANDER_STORAGE_ROW 2
#define STORAGE_KEY_NUMBER 110
#define DIFFICULTY_LEVELS 3
char lander_difficulty_names[DIFFICULTY_LEVELS][7] = {
"NOrMAL", "HArd ", "HArdEr"
};
#define MONSTER_TYPES 4
char lander_monster_names[MONSTER_TYPES][7] = {
"mOnStr", "6Erbil", "HAmStr", "Rabbit"
};
#define MONSTER_ACTIONS 8
char lander_monster_actions[MONSTER_ACTIONS][7] = {
"HUn6ry", " EAtS", "6Reedy", "annoYd", "nASty ", "SAVOry", "HO66SH", " pI66Y"
};
// --------------
// Custom methods
// --------------
static int gen_random_int (int16_t lower, int16_t upper) {
int range;
int retVal;
range = upper - lower + 1;
if ( range < 2 ) range = 2;
// Emulator: use rand. Hardware: use arc4random.
#if __EMSCRIPTEN__
retVal = rand() % range;
#else
retVal = arc4random_uniform(range);
#endif
retVal += lower;
return retVal;
}
static uint8_t assignProb ( uint8_t lowerProb, uint8_t upperProb, int16_t lowerSpeed, int16_t upperSpeed, int16_t actSpeed ) {
float probRange, speedRange;
float ratio, probFloat;
int probInt;
speedRange = upperSpeed - lowerSpeed;
if (speedRange<1.0) speedRange = 1.0;
probRange = upperProb - lowerProb;
ratio = ( (float) actSpeed - (float) lowerSpeed ) / speedRange;
probFloat = (float) lowerProb + ( ratio * probRange );
probInt = (int) ( probFloat + 0.5 );
probInt = min ( probInt, upperProb );
probInt = max ( probInt, lowerProb );
return (uint8_t) probInt;
}
static void write_to_lander_EEPROM(lander_state_t *state) {
uint8_t output_array [ 3 ];
output_array [ 0 ] = STORAGE_KEY_NUMBER;
output_array [ 1 ] = state->hero_counter;
output_array [ 2 ] = state->legend_counter;
watch_storage_erase ( LANDER_STORAGE_ROW );
watch_storage_sync ( );
watch_storage_write ( LANDER_STORAGE_ROW, 0, output_array, 3 );
}
// ---------------------------
// Standard watch face methods
// ---------------------------
void lander_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(lander_state_t));
memset(*context_ptr, 0, sizeof(lander_state_t));
lander_state_t *state = (lander_state_t *)*context_ptr;
state->led_enabled = false;
}
// Emulator only: Seed random number generator
#if __EMSCRIPTEN__
srand(time(NULL));
#endif
}
void lander_face_activate(void *context) {
lander_state_t *state = (lander_state_t *)context;
char buf [ 7 ];
state->mode = MODE_WAITING_TO_START;
state->led_active = false;
state->reset_counter = 0;
watch_clear_all_indicators ( );
uint32_t offset = 0;
uint32_t size = 3;
uint8_t stored_data [ size ];
// See if the hero_counter was ever written to EEPROM storage
watch_storage_read (LANDER_STORAGE_ROW, offset, stored_data, size);
if (stored_data[0] == STORAGE_KEY_NUMBER )
{
state->hero_counter = stored_data [1]; // There's real data in there.
state->legend_counter = stored_data [2];
}
else
{
state->hero_counter = 0; // Nope. Nothing there.
state->legend_counter = 0;
write_to_lander_EEPROM(state); // Initial EEPROM tracking data.
}
state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL;
state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit
// Fancy intro
if ( state->legend_counter == 0 ) watch_display_text(WATCH_POSITION_TOP_LEFT, "LA");
else watch_display_text(WATCH_POSITION_TOP_LEFT, "LE");
if ( ( state->hero_counter == 0 ) || ( state->hero_counter >= 40 ) ) watch_display_text ( WATCH_POSITION_TOP_RIGHT, " ");
else
{
sprintf ( buf, "%2d", state->hero_counter );
watch_display_text ( WATCH_POSITION_TOP_RIGHT, buf);
}
if ( state->hero_counter >= 100 ) sprintf ( buf, "Str%3d", state->hero_counter );
else if ( state->hero_counter >= 40 ) sprintf ( buf, "Strb%2d", state->hero_counter );
else if ( state->hero_counter >= LEVEL_STARBUCK ) sprintf ( buf, "StrbUC" );
else if ( state->hero_counter >= LEVEL_ACE ) sprintf ( buf, " ACE " ); // This human is good
else if ( state->difficulty_level == 0 ) sprintf ( buf, " " );
else sprintf ( buf, "%s", lander_difficulty_names[state->difficulty_level] );
watch_display_text ( WATCH_POSITION_BOTTOM, buf);
if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
}
bool lander_face_loop(movement_event_t event, void *context) {
lander_state_t *state = (lander_state_t *)context;
char buf [ 20 ]; // [11] is more correct and works; compiler too helpful.
switch (event.event_type) {
case EVENT_TICK:
state->tick_counter++;
if ( state->mode == MODE_PLAYING ) {
int16_t accel = state->gravity;
bool gas_pedal_on = HAL_GPIO_BTN_ALARM_read() || HAL_GPIO_BTN_LIGHT_read();
if ( gas_pedal_on && ( state->fuel_remaining > 0 ) ) {
accel = ENGINE_THRUST + state->gravity; // Gravity is negative
state->fuel_remaining--; // Used 1 fuel unit
watch_set_indicator ( WATCH_INDICATOR_LAP );
// Low fuel warning indicators
if ( state->fuel_remaining == ( 3 * LANDER_TICK_FREQUENCY ) ) { // 3 seconds of fuel left
watch_set_indicator ( WATCH_INDICATOR_SIGNAL );
watch_set_indicator ( WATCH_INDICATOR_BELL );
watch_set_indicator ( WATCH_INDICATOR_PM );
watch_set_indicator ( WATCH_INDICATOR_24H );
}
else if ( state->fuel_remaining == 0 ) { // 0 seconds of fuel left, empty!
watch_clear_all_indicators ( );
}
}
else {
watch_clear_indicator ( WATCH_INDICATOR_LAP );
}
state->speed += accel;
state->height += state->speed;
if ( state->height > 971 * 80 ) { // Escape height
watch_clear_all_indicators ();
watch_display_text( WATCH_POSITION_BOTTOM, "ESCAPE" );
state->tick_counter = 0;
state->mode = MODE_WAITING_TO_START;
}
else if ( state->height <= 0 ) { // Touchdown
state->tick_counter = 0;
state->mode = MODE_TOUCHDOWN_BLANK;
}
else {
// Update height display
sprintf ( buf, "%4d", (int) ( state->height / GRANUL ) );
watch_display_text( WATCH_POSITION_BOTTOM, buf );
}
}
else if ( state->mode == MODE_TOUCHDOWN_BLANK ) {
// Blank display on touchdown
if ( state->tick_counter == 1 ) {
watch_clear_all_indicators ();
watch_display_text( WATCH_POSITION_BOTTOM, " " );
// Also calc fuel score now.
float fuel_score_float;
uint16_t fuel_used;
fuel_used = state->fuel_start - state->fuel_remaining;
fuel_score_float = (float) fuel_used / (float) state->fuel_tpl;
state->fuel_score = (int) (fuel_score_float * 100.0 + 0.5);
if ( state->legend_counter == 0 ) state->fuel_score -= 8; // First Earth is easier
// Monitor reset_counter
if ( fuel_used >= 1 ) state->reset_counter = 0;
else state->reset_counter++;
if ( state->reset_counter >= 3 ) {
state->hero_counter = 0;
state->difficulty_level = 0;
if ( state->reset_counter >= 6 ) state->legend_counter = 0;
watch_display_text(WATCH_POSITION_BOTTOM, "rESET ");
write_to_lander_EEPROM(state);
}
}
// Wait until time for next display
if ( state->tick_counter >= ( 1 * LANDER_TICK_FREQUENCY ) ) {
state->tick_counter = 0;
state->mode = MODE_DISPLAY_FINAL_STATUS;
}
}
else if ( state->mode == MODE_DISPLAY_FINAL_STATUS ) {
bool last_pass = false;
if ( state->tick_counter >= LANDER_TICK_FREQUENCY ) last_pass = true;
// Show final status
if ( state->tick_counter == 1 ) {
// Calculate many attributes
// 1) Major crash: bug, crater, vaporized (gone).
// 2) Rank ship's health 0 to 8
// 3) Crew fatalities and injuries
// 4) Special conditions: hero
// 5) Set fuel conservation indicators as appropriate
// 6) Set coffee maker OK indicator as appropriate
// 7) Green light if ship intact
// 8) Set standard display if not preempted.
bool allDone;
int16_t finalSpeed, boostedSpeed, levelsDamage;
int8_t shipsHealth, myRand;
uint8_t fatalities, probFatal, probInjury;
uint8_t i;
allDone = false;
// Easiest implementation for difficulty_level is to increase touchdown speed above actual.
finalSpeed = abs ( state->speed ) + state->difficulty_level * 4;
// First Earth is a bit easier than all the others
if ( state->legend_counter == 0 ) finalSpeed -= 2;
// 1) Major crash: bug, crater, vaporized (gone).
if ( finalSpeed >= SPEED_MAJOR_CRASH ) {
allDone = true;
shipsHealth = -1;
if ( finalSpeed >= ( SPEED_MAJOR_CRASH + 2 * MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, "GOnE " );
else if ( finalSpeed >= ( SPEED_MAJOR_CRASH + MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, " CrAtr" );
else sprintf ( buf, " bU6" );
}
// 2) Rank ship's health 0 to 8
if (!allDone) {
boostedSpeed = finalSpeed + SPEED_LEVEL_INCREMENTS - 1;
levelsDamage = (int) ( ( boostedSpeed - SPEED_NO_DAMAGE ) / SPEED_LEVEL_INCREMENTS );
shipsHealth = 8 - levelsDamage;
shipsHealth = min ( shipsHealth, 8 ); // Keep between 0 and 8
shipsHealth = max ( shipsHealth, 0 );
}
state->ships_health = shipsHealth; // Remember ships health
// 3) Crew fatalities and injuries
if (!allDone) {
// Fatalies
probFatal = assignProb ( 0, 92, SPEED_FATALITY_NONE, SPEED_FATALITY_ALL, finalSpeed );
// Injuries
if ( finalSpeed <= SPEED_INJURY_FULCRUM ) {
probInjury = assignProb ( 0, INJURY_FULCRUM_PROB, SPEED_INJURY_NONE, SPEED_INJURY_FULCRUM, finalSpeed );
} else {
probInjury = assignProb ( INJURY_FULCRUM_PROB, 96, SPEED_INJURY_FULCRUM, SPEED_FATALITY_ALL, finalSpeed );
}
fatalities = 0;
state->injured = 0;
for ( i = 0; i < CREWS_COMPLIMENT; i++ ) {
myRand = gen_random_int ( 1, 100 );
if ( myRand <= probFatal ) fatalities++;
else if ( myRand <= probInjury ) state->injured++;
}
state->uninjured = CREWS_COMPLIMENT - fatalities - state->injured;
}
// 4) Special conditions: hero
if (!allDone) {
if ( (shipsHealth>=8) && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) {
state->hero_counter++;
if ( state->hero_counter==1 ) sprintf ( buf, "HErO " );
else if ( state->hero_counter == LEVEL_ACE ) sprintf ( buf, " ACE " );
else if ( state->hero_counter == LEVEL_STARBUCK ) sprintf ( buf, "STrbUC" );
else if ( state->hero_counter>99 ) sprintf ( buf, "HEr%3d", state->hero_counter );
else sprintf ( buf, "HErO%2d", state->hero_counter ); // Typical case
allDone = true;
// Two rule sets for finding Earth. Alternate between easy and hard.
int8_t my_odds, temp;
if ( state->legend_counter %2 == 0 ) my_odds = (int8_t) state->hero_counter - LEVEL_STARBUCK; // Easy
else {
temp = ( state->hero_counter - LEVEL_STARBUCK ) + HARD_EARTH_INCREMENTS - 1;
my_odds = temp / HARD_EARTH_INCREMENTS;
my_odds = min ( my_odds, MAX_HARD_EARTH_CHANCE );
}
// Display odds in weekday region if positive value
if ( my_odds > 0 ) {
char buff3 [ 5 ];
sprintf ( buff3, "%2d", my_odds );
watch_display_text( WATCH_POSITION_TOP_RIGHT, buff3 );
} else watch_display_text( WATCH_POSITION_TOP_RIGHT, " " );
if ( my_odds >= gen_random_int ( 1, 200 ) ) { // EARTH!!!! The final objective.
sprintf ( buf, "EArTH " ); // 17% within 8, 50% by 16, 79% by 24, 94% by 32 <- easy mode
state->hero_counter = 0;
state->legend_counter++;
}
// Recalculate difficulty level base on new hero_counter.
state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL;
state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit
// Write to EEPROM
write_to_lander_EEPROM(state);
}
}
// 5) Set fuel conservation indicators as appropriate
if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) watch_set_indicator ( WATCH_INDICATOR_LAP );
if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GREAT ) ) watch_set_indicator ( WATCH_INDICATOR_24H );
if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GOOD ) ) watch_set_indicator ( WATCH_INDICATOR_PM );
// 6) Set coffee maker OK indicator as appropriate
if ( shipsHealth >= 5 || ( shipsHealth >= 0 && ( gen_random_int ( 0, 3 ) != 1 ) ) ){
watch_set_indicator ( WATCH_INDICATOR_SIGNAL );
}
// 7) Green light if ship intact
if ( shipsHealth >= 8 && state->led_enabled) {
watch_set_led_green ( );
state->led_active = true;
}
// 8) Set standard display if not preempted.
if (!allDone) {
if ( ( state->injured > 0 ) || ( state->uninjured == 0 ) ) {
sprintf ( buf, "%d %2d%2d", shipsHealth, state->uninjured, state->injured );
}
else {
sprintf ( buf, "%d %2d ", shipsHealth, state->uninjured );
}
}
// Display final status.
watch_display_text(WATCH_POSITION_BOTTOM, buf );
} // End if tick_counter == 1
// Major crash - ship burning with red LED.
if ( state->ships_health < 0 && state->led_enabled) {
if ( ( gen_random_int ( 0, 1 ) != 1 ) && !last_pass ) { // Always off on last pass
// Turn on red LED.
watch_set_led_red ( );
state->led_active = true;
} else {
watch_set_led_off ( );
}
}
// Wait long enough, then allow waiting for next game.
if ( last_pass ) {
watch_set_led_off ( );
// No change to display text, allow new game to start.
state->mode = MODE_WAITING_TO_START;
// Unless it's time for monsters
uint8_t survivors = state->injured + state->uninjured;
if ( ( state->ships_health >= 0 ) && ( survivors > 0 ) &&
( gen_random_int ( -1, 3 ) >= state->ships_health ) ) {
state->mode = MODE_MONSTER;
state->tick_counter = 0;
state->monster_type = gen_random_int ( 0, MONSTER_TYPES - 1 );
}
}
} // End if MODE_DISPLAY_FINAL_STATUS
else if ( state->mode == MODE_DISPLAY_SKILL_LEVEL ) {
// Display skill level
if ( state->tick_counter == 1 ) {
sprintf ( buf, " %d", state->skill_level );
watch_display_text ( WATCH_POSITION_TOP_RIGHT, buf );
sprintf ( buf, " %d ", state->skill_level );
watch_display_text ( WATCH_POSITION_BOTTOM, buf );
}
// Wait long enough, then start game.
if ( state->tick_counter >= ( 2.0 * LANDER_TICK_FREQUENCY ) ) {
state->tick_counter = 0;
// Houston, WE ARE LAUNCHING NOW....
state->mode = MODE_PLAYING;
}
}
else if ( state->mode == MODE_FIND_EARTH_MESSAGE ) {
// Display "Find" then "Earth"
if ( state->tick_counter == 1 ) {
sprintf ( buf, " FInd " );
watch_display_text ( WATCH_POSITION_TOP_RIGHT, " " );
watch_display_text ( WATCH_POSITION_BOTTOM, buf );
}
if ( state->tick_counter == (int) ( 1.5 * LANDER_TICK_FREQUENCY + 1 ) ) {
sprintf ( buf, "EArTH " );
watch_display_text ( WATCH_POSITION_TOP_RIGHT, " " );
watch_display_text ( WATCH_POSITION_BOTTOM, buf );
}
// Wait long enough, then display skill level.
if ( state->tick_counter >= ( 3 * LANDER_TICK_FREQUENCY ) ) {
state->tick_counter = 0;
state->mode = MODE_DISPLAY_SKILL_LEVEL;
}
}
else if ( state->mode == MODE_MONSTER ) {
if ( state->tick_counter == 1 ) watch_display_text ( WATCH_POSITION_BOTTOM, lander_monster_names[state->monster_type] );
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS + 1 ) {
uint8_t my_rand;
my_rand = gen_random_int ( 0 , MONSTER_ACTIONS - 1 );
watch_display_text ( WATCH_POSITION_BOTTOM, lander_monster_actions[my_rand] );
}
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 ) { // Display 1st monster character
sprintf ( buf, "%s", lander_monster_names[state->monster_type] );
buf [1] = 0;
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 1 ) { // Display current population, close mouth
sprintf ( buf, " c%2d%2d", state->uninjured, state->injured );
watch_display_text ( WATCH_POSITION_BOTTOM, buf );
}
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 3 ) watch_display_character ( 'C', 5 ); // Open mouth
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 5 ) {
// Decision to: continue loop, end loop or eat astronaut
uint8_t survivors = state->injured + state->uninjured;
uint8_t myRand = gen_random_int ( 0, 16 );
if ( survivors == 0 ) state->mode = MODE_WAITING_TO_START;
else if ( myRand <= 1 ) { // Leave loop with survivors
sprintf ( buf, "%d %2d%2d", state->ships_health, state->uninjured, state->injured );
watch_display_text ( WATCH_POSITION_BOTTOM, buf);
state->mode = MODE_WAITING_TO_START;
} else if ( myRand <= 11 ) state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Do nothing, loop continues
else { // Eat an astronaut - welcome to the space program!
if ( state->injured > 0 && state->uninjured > 0 ) {
if ( gen_random_int ( 0,1 ) == 0 ) state->injured--;
else state->uninjured--;
}
else if ( state->injured > 0 ) state->injured--;
else state->uninjured--;
state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Re-display
}
}
else if ( state->tick_counter >= MONSTER_DISPLAY_TICKS * 4 ) state->mode = MODE_WAITING_TO_START; // Safety
} // End if MODE_MONSTER
break; // End case EVENT_TICK
case EVENT_ALARM_BUTTON_DOWN:
if ( state->mode == MODE_WAITING_TO_START ) {
// That was the go signal - start a new game!!
float numerator, denominator, timeSquared;
int16_t gravity, thrust;
float myTime, distToTop, fuel_mult;
uint8_t skill_level;
int32_t tplTop; // Top lander height for TPL calculations
movement_request_tick_frequency(LANDER_TICK_FREQUENCY);
watch_set_led_off ( ); // Safety
watch_clear_all_indicators ( );
// Randomize starting parameters
state->height = gen_random_int ( 131, 181 ) * 80;
// Per line below; see Mars Orbiter September 23, 1999
if ( gen_random_int ( 0, 8 ) == 5 ) state->height = gen_random_int ( 240, 800 ) * 80;
state->speed = gen_random_int ( -120, 35 ); // Positive is up
state->gravity = gen_random_int ( -3, -2 ) * 2; // negative downwards value
skill_level = gen_random_int ( 1, 4 ); // Precursor to fuel allocation
// Theoretical Perfect Landing (TPL) calculations start here.
myTime = (float) state->speed / (float) state->gravity; // How long to reach this speed? Don't care which way sign is.
distToTop = fabs ( 0.5 * state->gravity * myTime * myTime );
tplTop = (int) ( state->height + distToTop + 0.5 ); // Theoretical highest point based on all of speed, height and gravity.
// Time squared = ( 2 * grav * height ) / ( t*t + g*t ), where t is net acceleration with thrust on.
gravity = abs ( state->gravity );
thrust = ENGINE_THRUST + state->gravity;
numerator = 2.0 * (float) gravity * (float) tplTop;
denominator = thrust * thrust + thrust * gravity;
timeSquared = numerator / denominator;
state->fuel_tpl = (int) ( sqrt ( timeSquared ) + 0.5 ); // Fuel required for theoretical perfect landing (TPL).
if ( skill_level == 1 ) fuel_mult = 4.0; // TPL + 300%
else if ( skill_level == 2 ) fuel_mult = 2.5; // TPL + 150%
else if ( skill_level == 3 ) fuel_mult = 1.6; // TPL + 60%
else fuel_mult = 1.3; // TPL + 30%
state->fuel_start = state->fuel_tpl * fuel_mult;
state->fuel_remaining = state->fuel_start;
state->skill_level = skill_level;
state->tick_counter = 0;
if ( gen_random_int ( 1, 109 ) != 37 ) {
// Houston, approaching launch....
state->mode = MODE_DISPLAY_SKILL_LEVEL;
}
else state->mode = MODE_FIND_EARTH_MESSAGE;
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
if ( state->mode == MODE_WAITING_TO_START ) {
// Display difficulty level
watch_display_text(WATCH_POSITION_BOTTOM, lander_difficulty_names [state->difficulty_level]);
}
break;
case EVENT_LIGHT_LONG_PRESS:
if ( state->mode != MODE_WAITING_TO_START ) break;
state->led_enabled = !state->led_enabled;
if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
break;
case EVENT_LIGHT_LONG_UP:
if ( ( state->mode == MODE_WAITING_TO_START ) && ( state->legend_counter > 0 ) ) {
if ( state->legend_counter > 9 ) sprintf (buf,"EArt%2d", state->legend_counter );
else sprintf (buf,"EArth%d", state->legend_counter );
// Display legend counter
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
break;
default:
movement_default_loop_handler(event);
break;
}
if ( !state->led_active ) return true;
else return false;
}
void lander_face_resign(void *context) {
(void) context;
watch_set_led_off ( );
}
+152
View File
@@ -0,0 +1,152 @@
/*
* MIT License
*
* Copyright (c) 2024 Klingon Jane
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef LANDER_FACE_H_
#define LANDER_FACE_H_
#include "movement.h"
/*
My remake of a classic planet landing game.
Objective: Safely land the Cringeworthy.
Use your limited fuel supply to achieve a soft touch-down.
End scenarios and ship's health:
Hero They name this planet after you.
8 Life is very cozy.
7
6
5 Life is tolerable, plus some creature comforts
4
3 Marooned.
2
1
0 Ship destroyed. Life is harsh, no shelter. Giant hamsters are cute. **
Bug As in squished.
Crater They name this crater after you.
Gone As in vapourized.
Landing display format is:
Ship's health, intact crewmen, injured crewmen.
Additional data:
Crew's compliment: 13.
Low fuel warning icons: activates when 3 seconds of full thrust remains.
** Yes, hamsters are very cute. However; some eating of astronauts may occur.
Starting velocity, height and gravity are randomized each scenario.
Fuel levels randomly assigned from 1 to 4 (hardest) to match starting parameters.
A safe landing is always possible.
End of game icons:
LAP - Fantastic budgeting of fuel supply ( Required for heroic landing status. )
24H - Great budgeting of fuel supply
PM - Good budgeting of fuel supply
SIGNAL - The combination coffee and tea maker survived
Landings get progressively harder with the number of heroic landings made.
Number of heroic landings are remembered.
Heroic
Landings Status
0 Normal
3 Hard ( first difficulty increase )
6 Harder ( final difficulty increase )
8 Ace
11 ??????
Save yourself. Save the coffee maker.
END of standard training manual
*/
/*
What is really going on here?
The fleet is lost. You are a newbie pilot making a name for yourself.
Objective: Find Earth.
After reaching ?????? status, future heroic sorties will have 'some' chance in 200
of finding Earth.
Your chances improve by 1 chance in 200 for each subsequent Heroic Landing (HL).
Completing HL 12 will give you 1 chance in 200, for that landing.
HL 13 will give you 2 chances in 200, for that landing.
HL 14 will give you 3 chances in 200, for that landing.
HL 20 will give you 9 chances in 200, for that landing, and so on.
At these higher levels, your chances in 200 are displayed in the upper right corner on a heroic landing.
For wannabe pilots only: The HL counter can be reset by crashing three consecutive
missions without touching the thrust button. ( 6 to reset Earth-found counter )
Find Earth. Save Humanity.
*/
typedef struct {
int32_t height;
int16_t speed; // Positive is up
uint16_t tick_counter; // For minimum delays
uint16_t fuel_start;
uint16_t fuel_remaining;
uint16_t fuel_tpl; // Fuel required for theoretical perfect landing
uint16_t fuel_score; // 100 is perfect; higher is less perfect
int8_t gravity; // negative downwards value
bool led_enabled; // Can the led be turned on?
bool led_active; // Did we use it this scenario?
uint8_t mode; // 0 Pre-launch waiting, 1 show level, 2 playing, 3 touchdown blank, 4 final display, 5 monster
uint8_t skill_level; // 1 thru 4. Dictates fuel alloted
int8_t ships_health; // 0 thru 8. -1 = major crash
uint8_t hero_counter; // Total heroic landings ever
uint8_t legend_counter; // Historic events counter ( Earth )
uint8_t difficulty_level; // Based on hero_counter
uint8_t reset_counter; // Can reset hero_counter by crashing using zero fuel several consecutive scenarios
uint8_t monster_type; // Which monster is hungry?
uint8_t uninjured; // OK survivors
uint8_t injured; // Hurt survivors
} lander_state_t;
void lander_face_setup(uint8_t watch_face_index, void ** context_ptr);
void lander_face_activate(void *context);
bool lander_face_loop(movement_event_t event, void *context);
void lander_face_resign(void *context);
#define lander_face ((const watch_face_t){ \
lander_face_setup, \
lander_face_activate, \
lander_face_loop, \
lander_face_resign, \
NULL, \
})
#endif // LANDER_FACE_H_
+12 -3
View File
@@ -184,10 +184,19 @@ bool moon_phase_face_loop(movement_event_t event, void *context) {
state->offset += 86400;
_update(state, state->offset);
break;
case EVENT_ALARM_LONG_PRESS:
state->offset = 0;
case EVENT_ALARM_LONG_PRESS:
state->offset = 0;
_update(state, state->offset);
break;
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_BUTTON_UP:
state->offset -= 86400;
_update(state, state->offset);
break;
case EVENT_LIGHT_LONG_PRESS:
movement_illuminate_led();
break;
case EVENT_TIMEOUT:
// QUESTION: Should timeout reset offset to 0?
break;
@@ -47,6 +47,9 @@
* each button press, and both the text and the graphical representation will
* display the moon phase for that day. Try pressing the Alarm button 27 times
* now, just to visualize what the moon will look like over the next month.
* Pressing the Light button will move back in time.
*
* Holding the Light button will illuminate the display.
*/
#include "movement.h"
+583
View File
@@ -0,0 +1,583 @@
/*
* MIT License
*
* Copyright (c) 2024 <David Volovskiy>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include <string.h>
#include "ping_face.h"
#include "delay.h"
#include "watch_common_display.h"
typedef enum {
PADDLE_RETRACTED = 0,
PADDLE_EXTENDING,
PADDLE_EXTENDED,
PADDLE_RETRACTING,
} PingPaddleState;
typedef enum {
SCREEN_TITLE = 0,
SCREEN_SCORE,
SCREEN_PLAYING,
SCREEN_LOSE,
SCREEN_COUNT
} PingCurrScreen;
typedef enum {
DIFF_BABY = 0, // FREQ_BABY FPS
DIFF_EASY, // FREQ_EASY FPS
DIFF_NORM, // FREQ_NORM FPS
DIFF_HARD, // FREQ_NORM FPS, smaller travel-distance for ball
DIFF_COUNT
} PingDifficulty;
typedef enum {
RESULT_LOSE = -1,
RESULT_NONE = 0,
RESULT_HIT = 1,
RESULT_FIRST_HIT = 2,
} PingResult;
#define FREQ_BABY 2
#define FREQ_EASY 4
#define FREQ_NORM 8
#define BALL_POS_MAX 11
#define BALL_OFF_SCREEN 100
#define MAX_HI_SCORE 9999 // Max hi score to store and display on the title screen.
#define MAX_DISP_SCORE 39 // The top-right digits can't properly display above 39
typedef struct {
uint8_t ball_pos; // 0 to 11; 0 is the bottom-right and 11 is the top right.
// | 6 | 7 | 8 | 9 | 10 | 11 |
// | 5 | 4 | 3 | 2 | 1 | 0 |
PingPaddleState paddle_pos;
uint8_t ball_char_pos; // Derived from ball_pos
bool ball_is_clockwise;
bool ball_is_moving;
uint16_t curr_score;
PingCurrScreen curr_screen;
bool paddle_hit;
bool paddle_released;
uint8_t curr_freq;
bool moving_from_tap;
} game_state_t;
static game_state_t game_state;
static int8_t _ticks_show_title = 0;
static bool _is_custom_lcd;
static int8_t start_tune[] = {
BUZZER_NOTE_C5, 15,
BUZZER_NOTE_E5, 15,
BUZZER_NOTE_G5, 15,
0
};
static int8_t lose_tune[] = {
BUZZER_NOTE_D3, 10,
BUZZER_NOTE_C3SHARP_D3FLAT, 10,
BUZZER_NOTE_C3, 10,
0
};
static uint8_t ball_pos_to_char_pos(uint8_t ball_pos) {
switch (ball_pos)
{
case 5:
case 6:
return 4;
case 4:
case 7:
return 5;
case 3:
case 8:
return 6;
case 2:
case 9:
return 7;
case 1:
case 10:
return 8;
case 0:
case 11:
return 9;
default:
return BALL_OFF_SCREEN;
}
}
static bool paddle_and_ball_on_same_segment(void) {
if (game_state.paddle_pos == PADDLE_EXTENDED) {
if (game_state.ball_pos == 9 || game_state.ball_pos == 2) {
return true;
}
}
else if (game_state.paddle_pos == PADDLE_EXTENDING || game_state.paddle_pos == PADDLE_RETRACTING) {
if (game_state.ball_pos == 10 || game_state.ball_pos == 1) {
return true;
}
}
else if (game_state.paddle_pos == PADDLE_RETRACTED) {
if (game_state.ball_pos == 11 || game_state.ball_pos == 0) {
return true;
}
}
return false;
}
static bool paddle_hit_ball(void) {
if (game_state.paddle_pos == PADDLE_EXTENDED) {
if (game_state.ball_pos >= 9 && game_state.ball_is_clockwise) {
return true;
}
if (game_state.ball_pos <= 2 && !game_state.ball_is_clockwise) {
return true;
}
}
else if (game_state.paddle_pos == PADDLE_EXTENDING) {
if (game_state.ball_pos >= 10 && game_state.ball_is_clockwise) {
return true;
}
if (game_state.ball_pos <= 1 && !game_state.ball_is_clockwise) {
return true;
}
}
return false;
}
static uint8_t get_next_ball_pos(bool ball_hit, uint8_t difficulty) {
int8_t offset_next;
if (ball_hit) {
bool ball_on_top = game_state.ball_pos > 5;
game_state.ball_is_clockwise = !ball_on_top;
// ball is at the same frame as the paddle
if (game_state.paddle_pos == PADDLE_EXTENDED) {
return ball_on_top ? 9 : 2;
} else if (game_state.paddle_pos == PADDLE_EXTENDING) {
return ball_on_top ? 10 : 1;
}
}
if (game_state.ball_is_clockwise) {
offset_next = 1;
} else {
offset_next = -1;
}
int8_t next_pos = game_state.ball_pos + offset_next;
if (next_pos > BALL_POS_MAX || next_pos < 0) {
return BALL_OFF_SCREEN;
}
if (difficulty == DIFF_HARD) {
if (next_pos == 4) {
next_pos = 8;
} else if (next_pos == 7) {
next_pos = 3;
}
}
return next_pos;
}
static void display_ball(void) {
uint8_t char_pos = ball_pos_to_char_pos(game_state.ball_pos);
uint8_t char_display;
bool overlap = paddle_and_ball_on_same_segment();
if (game_state.ball_pos > 5) {
if (overlap) {
char_display = 'q';
} else {
char_display = '#';
}
} else {
if (!_is_custom_lcd && (char_pos == 4 || char_pos == 6)) {
char_display = 'n'; // No need to check for overlap on these segments
} else {
if (overlap) {
char_display = 'd';
} else {
char_display = 'o';
}
}
}
watch_display_character(char_display, char_pos);
}
static PingResult update_ball(uint8_t difficulty) {
bool ball_hit = paddle_hit_ball();
bool first_hit = false;
if (!game_state.ball_is_moving) {
if (ball_hit) {
game_state.ball_is_moving = true;
first_hit = true;
} else {
return RESULT_NONE;
}
}
game_state.ball_pos = get_next_ball_pos(ball_hit, difficulty);
if (game_state.ball_pos == BALL_OFF_SCREEN) {
return RESULT_LOSE;
}
display_ball();
if (ball_hit) {
return first_hit ? RESULT_FIRST_HIT : RESULT_HIT;
} else {
return RESULT_NONE;
}
}
static void display_paddle(void) {
switch (game_state.paddle_pos)
{
case PADDLE_EXTENDING:
case PADDLE_RETRACTING:
watch_display_character('-', 9);
watch_display_character('1', 8);
break;
case PADDLE_EXTENDED:
watch_display_character('-', 9);
watch_display_character('-', 8);
watch_display_character('1', 7);
break;
case PADDLE_RETRACTED:
default:
watch_display_character('1', 9);
break;
}
}
static void update_paddle(void) {
switch (game_state.paddle_pos)
{
case PADDLE_RETRACTED:
if (game_state.paddle_hit) {
game_state.paddle_pos = PADDLE_EXTENDING;
}
break;
case PADDLE_EXTENDING:
if (!game_state.moving_from_tap && !HAL_GPIO_BTN_ALARM_read()) {
game_state.paddle_pos = PADDLE_RETRACTED;
watch_display_character(' ', 8);
game_state.moving_from_tap = false;
} else {
game_state.paddle_pos = PADDLE_EXTENDED;
}
break;
case PADDLE_EXTENDED:
game_state.paddle_pos = PADDLE_RETRACTING;
watch_display_character(' ', 7);
break;
case PADDLE_RETRACTING:
game_state.paddle_pos = PADDLE_RETRACTED;
watch_display_character(' ', 8);
game_state.moving_from_tap = false;
break;
default:
break;
}
game_state.paddle_hit = false;
display_paddle();
}
static inline bool paddle_is_extending(void) {
return game_state.paddle_pos == PADDLE_EXTENDING || game_state.paddle_pos == PADDLE_EXTENDED;
}
static void display_score(uint8_t score) {
char buf[3];
score %= (MAX_DISP_SCORE + 1);
sprintf(buf, "%2d", score);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
static void add_to_score(ping_state_t *state) {
if (game_state.curr_score <= MAX_HI_SCORE) {
game_state.curr_score++;
if (game_state.curr_score > state -> hi_score)
state -> hi_score = game_state.curr_score;
}
display_score(game_state.curr_score);
}
static void check_and_reset_hi_score(ping_state_t *state) {
// Resets the hi score at the beginning of each month.
watch_date_time_t date_time = movement_get_local_date_time();
if ((state -> year_last_hi_score != date_time.unit.year) ||
(state -> month_last_hi_score != date_time.unit.month))
{
// The high score resets itself every new month.
state -> hi_score = 0;
state -> year_last_hi_score = date_time.unit.year;
state -> month_last_hi_score = date_time.unit.month;
}
}
static void display_difficulty(uint16_t difficulty) {
static const char *labels[] = {
[DIFF_BABY] = " b",
[DIFF_EASY] = " E",
[DIFF_NORM] = " N",
[DIFF_HARD] = " H"
};
watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]);
}
static void change_difficulty(ping_state_t *state) {
state -> difficulty = (state -> difficulty + 1) % DIFF_COUNT;
display_difficulty(state -> difficulty);
if (state -> soundOn) {
if (state -> difficulty == 0) watch_buzzer_play_note(BUZZER_NOTE_B4, 30);
else watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
}
}
static void display_sound_indicator(bool soundOn) {
if (soundOn) {
watch_set_indicator(WATCH_INDICATOR_BELL);
} else {
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
}
static void toggle_sound(ping_state_t *state) {
state -> soundOn = !state -> soundOn;
display_sound_indicator(state -> soundOn);
if (state -> soundOn) {
watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
}
}
static void enable_tap_control(ping_state_t *state) {
if (!state->tap_control_on) {
movement_enable_tap_detection_if_available();
state->tap_control_on = true;
}
}
static void disable_tap_control(ping_state_t *state) {
if (state->tap_control_on) {
movement_disable_tap_detection_if_available();
state->tap_control_on = false;
}
}
static void display_title(ping_state_t *state) {
movement_request_tick_frequency(1);
game_state.curr_screen = SCREEN_TITLE;
watch_clear_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP, "Ping", "PI ");
watch_display_text(WATCH_POSITION_BOTTOM, " Ping ");
display_sound_indicator(state -> soundOn);
_ticks_show_title = 1;
}
static void display_score_screen(ping_state_t *state) {
uint16_t hi_score = state -> hi_score;
uint8_t difficulty = state -> difficulty;
movement_request_tick_frequency(1);
bool sound_on = state -> soundOn;
memset(&game_state, 0, sizeof(game_state));
game_state.curr_screen = SCREEN_SCORE;
watch_set_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP, "PI ", "PI ");
if (hi_score > MAX_HI_SCORE) {
watch_display_text(WATCH_POSITION_BOTTOM, "HS --");
}
else {
char buf[10];
sprintf(buf, "HS%4d", hi_score);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
display_difficulty(difficulty);
display_sound_indicator(sound_on);
}
static void begin_playing(ping_state_t *state) {
game_state.curr_screen = SCREEN_PLAYING;
watch_clear_colon();
display_sound_indicator(state -> soundOn);
switch (state -> difficulty)
{
case DIFF_BABY:
game_state.curr_freq = FREQ_BABY;
break;
case DIFF_EASY:
game_state.curr_freq = FREQ_EASY;
break;
case DIFF_NORM:
case DIFF_HARD:
default:
game_state.curr_freq = FREQ_NORM;
break;
}
movement_request_tick_frequency(game_state.curr_freq);
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, " ");
game_state.paddle_pos = PADDLE_RETRACTED;
game_state.ball_pos = 1;
game_state.paddle_hit = false;
game_state.ball_is_moving = false;
game_state.ball_is_clockwise = false;
game_state.curr_score = 0;
display_paddle();
display_ball();
display_score( game_state.curr_score);
}
static void display_lose_screen(ping_state_t *state) {
game_state.curr_screen = SCREEN_LOSE;
game_state.curr_score = 0;
watch_clear_display();
watch_display_text(WATCH_POSITION_BOTTOM, " LOSE ");
if (state -> soundOn) {
watch_buzzer_play_sequence(lose_tune, NULL);
delay_ms(600);
}
}
static void update_game(ping_state_t *state) {
if (game_state.ball_is_moving) {
watch_display_character(' ', ball_pos_to_char_pos(game_state.ball_pos)); // remove the old ball.
}
update_paddle();
int game_result = update_ball(state -> difficulty);
if (game_result == RESULT_LOSE) {
display_lose_screen(state);
} else if (game_result == RESULT_HIT) {
add_to_score(state);
if (state -> soundOn) {
watch_buzzer_play_note(BUZZER_NOTE_C5, 60);
}
} else if (game_result == RESULT_FIRST_HIT && state -> soundOn) {
watch_buzzer_play_sequence(start_tune, NULL);
}
}
void ping_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(ping_state_t));
memset(*context_ptr, 0, sizeof(ping_state_t));
ping_state_t *state = (ping_state_t *)*context_ptr;
state->difficulty = DIFF_NORM;
state->tap_control_on = false;
}
}
void ping_face_activate(void *context) {
(void) context;
_is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM;
if (watch_sleep_animation_is_running()) {
watch_stop_blink();
}
}
bool ping_face_loop(movement_event_t event, void *context) {
ping_state_t *state = (ping_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
disable_tap_control(state);
check_and_reset_hi_score(state);
display_title(state);
break;
case EVENT_TICK:
switch (game_state.curr_screen)
{
case SCREEN_TITLE:
if (_ticks_show_title > 0) {_ticks_show_title--;}
else {
watch_clear_display();
display_score_screen(state);
}
case SCREEN_SCORE:
case SCREEN_LOSE:
break;
case SCREEN_PLAYING:
default:
update_game(state);
break;
}
break;
case EVENT_ALARM_BUTTON_UP:
case EVENT_LIGHT_BUTTON_UP:
switch (game_state.curr_screen) {
case SCREEN_SCORE:
enable_tap_control(state);
begin_playing(state);
break;
case SCREEN_TITLE:
enable_tap_control(state);
// fall through
case SCREEN_LOSE:
watch_clear_display();
display_score_screen(state);
default:
break;
}
break;
case EVENT_LIGHT_LONG_PRESS:
if (game_state.curr_screen == SCREEN_SCORE)
change_difficulty(state);
break;
case EVENT_SINGLE_TAP:
case EVENT_DOUBLE_TAP:
// Allow starting a new game by tapping.
if (game_state.curr_screen == SCREEN_SCORE) {
begin_playing(state);
break;
}
else if (game_state.curr_screen == SCREEN_LOSE) {
display_score_screen(state);
break;
}
else if (game_state.curr_screen == SCREEN_PLAYING) {
game_state.moving_from_tap = true;
game_state.paddle_hit = true;
}
break;
case EVENT_ALARM_BUTTON_DOWN:
if (game_state.curr_screen == SCREEN_PLAYING) {
game_state.moving_from_tap = false;
game_state.paddle_hit = true;
}
break;
case EVENT_ALARM_LONG_PRESS:
if (game_state.curr_screen == SCREEN_TITLE || game_state.curr_screen == SCREEN_SCORE)
toggle_sound(state);
break;
case EVENT_TIMEOUT:
disable_tap_control(state);
if (game_state.curr_screen != SCREEN_SCORE) {
display_score_screen(state);
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void ping_face_resign(void *context) {
ping_state_t *state = (ping_state_t *)context;
disable_tap_control(state);
}
+71
View File
@@ -0,0 +1,71 @@
/*
* MIT License
*
* Copyright (c) 2025 <David Volovskiy>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef PING_FACE_H_
#define PING_FACE_H_
#include "movement.h"
/*
PING face
I saw the face made on the Ollee watch and thought it'd be fun to have on my Sensorwatch.
https://www.instagram.com/reel/DNlTb-ERE1F/
On the title screen, you can select a difficulty by long-pressing LIGHT or toggle sound by long-pressing ALARM.
ALARM are used to paddle. Holding the ALARM button longer makes the paddle travel further.
If the accelerometer is installed, you can tap the screen to move the paddle. Paddle will travel its full distance when tapping is used.
High-score is displayed on the top-right on the title screen. During a game, the current score is displayed.
Difficulties:
Baby: 2 FPS
Easy: 4 FPS
Normal: 8 FPS
Hard: 8 FPS and the ball travels half the half the board.
*/
typedef struct {
uint16_t hi_score : 10;
uint8_t difficulty : 3;
uint8_t month_last_hi_score : 4;
uint8_t year_last_hi_score : 6;
uint8_t soundOn : 1;
uint8_t tap_control_on : 1;
uint8_t unused : 7;
} ping_state_t;
void ping_face_setup(uint8_t watch_face_index, void ** context_ptr);
void ping_face_activate(void *context);
bool ping_face_loop(movement_event_t event, void *context);
void ping_face_resign(void *context);
#define ping_face ((const watch_face_t){ \
ping_face_setup, \
ping_face_activate, \
ping_face_loop, \
ping_face_resign, \
NULL, \
})
#endif // ping_FACE_H_
@@ -80,6 +80,7 @@ static void pulsometer_display_measurement(pulsometer_state_t *pulsometer) {
char buf[5];
int16_t value = pulsometer->pulses;
if (value < 0) value = 0;
if (value > 9999) value = 9999;
snprintf(buf, sizeof(buf), "%-4hd", value);
+328
View File
@@ -0,0 +1,328 @@
/*
* 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 "delay.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) {
watch_clear_display();
if (state->playing_state != SIMON_NOT_PLAYING) {
sprintf(_simon_display_buf, "%2d", state->sequence_length);
watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf);
}
}
static void _simon_not_playing_display(simon_state_t *state) {
_simon_clear_display(state);
watch_display_text_with_fallback(WATCH_POSITION_TOP, "SIMON", "SI");
sprintf(_simon_display_buf, "%d", state->best_score);
watch_display_text(WATCH_POSITION_BOTTOM, _simon_display_buf);
if (!state->soundOff)
watch_set_indicator(WATCH_INDICATOR_BELL);
else
watch_clear_indicator(WATCH_INDICATOR_BELL);
if (!state->lightOff)
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
switch (state->mode)
{
case SIMON_MODE_EASY:
watch_display_text(WATCH_POSITION_SECONDS, " E");
break;
case SIMON_MODE_HARD:
watch_display_text(WATCH_POSITION_SECONDS, " H");
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) {
watch_clear_display();
if (note == SIMON_WRONG_NOTE) {
watch_display_text(WATCH_POSITION_TOP_LEFT, "OH");
watch_display_text(WATCH_POSITION_BOTTOM, "NOOOOO");
return;
}
sprintf(_simon_display_buf, "%2d", state->sequence_length);
watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf);
switch (note) {
case SIMON_LED_NOTE:
watch_display_text(WATCH_POSITION_TOP_LEFT, "LI");
break;
case SIMON_ALARM_NOTE:
watch_display_text(WATCH_POSITION_SECONDS, "AL");
break;
case SIMON_MODE_NOTE:
watch_display_text_with_fallback(WATCH_POSITION_HOURS, "Md", "DE");
break;
default:
break;
}
}
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) watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
delay_ms(_delay_beep);
break;
case SIMON_MODE_NOTE:
if (!state->lightOff) watch_set_led_red();
if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep);
delay_ms(_delay_beep);
break;
case SIMON_ALARM_NOTE:
if (!state->lightOff) watch_set_led_green();
if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep);
delay_ms(_delay_beep);
break;
case SIMON_WRONG_NOTE:
if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_A1, 800);
delay_ms(800);
break;
}
watch_set_led_off();
if (note != SIMON_WRONG_NOTE) {
_simon_clear_display(state);
if (!skip_rest) {
delay_ms((_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) 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();
}
+111
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_
@@ -26,6 +26,7 @@
#include <stdlib.h>
#include <string.h>
#include "simple_coin_flip_face.h"
#include "delay.h"
void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
@@ -36,7 +37,7 @@ void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr)
}
void simple_coin_flip_face_activate(void *context) {
simple_coin_flip_face_state_t *state = (simple_coin_flip_face_state_t *)context;
(void) context;
}
static uint32_t get_random(uint32_t max) {
@@ -48,7 +49,7 @@ static uint32_t get_random(uint32_t max) {
}
void draw_start_face() {
static void draw_start_face(void) {
watch_clear_display();
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_display_text(WATCH_POSITION_BOTTOM, " Flip");
@@ -57,7 +58,7 @@ void draw_start_face() {
}
}
void set_pixels(int pixels[3][4][2], int j_len) {
static void set_pixels(int pixels[3][4][2], int j_len) {
for(int loopruns = 0; loopruns<2; loopruns++) {
for(int i = 0; i<3; i++) {
watch_clear_display();
@@ -69,7 +70,7 @@ void set_pixels(int pixels[3][4][2], int j_len) {
}
}
void load_animation() {
static void load_animation(void) {
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
int j_len = 2;
int pixels[3][4][2] = {
@@ -114,6 +115,7 @@ void load_animation() {
}
static void _blink_face_update_lcd(simple_coin_flip_face_state_t *state) {
(void) state;
watch_clear_display();
load_animation();
watch_clear_display();
@@ -82,9 +82,8 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) {
}
watch_date_time_t date_time = movement_get_local_date_time(); // the current local date / time
watch_date_time_t utc_now = watch_utility_date_time_convert_zone(date_time, movement_get_current_timezone_offset(), 0); // the current date / time in UTC
watch_date_time_t scratch_time; // scratchpad, contains different values at different times
scratch_time.reg = utc_now.reg;
scratch_time.reg = date_time.reg;
// Weird quirky unsigned things were happening when I tried to cast these directly to doubles below.
// it looks redundant, but extracting them to local int16's seemed to fix it.
@@ -200,7 +199,7 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) {
}
// it's after sunset. we need to display sunrise/sunset for tomorrow.
uint32_t timestamp = watch_utility_date_time_to_unix_time(utc_now, 0);
uint32_t timestamp = watch_utility_date_time_to_unix_time(date_time, 0);
timestamp += 86400;
scratch_time = watch_utility_date_time_from_unix_time(timestamp, 0);
}
+16 -6
View File
@@ -110,6 +110,12 @@ static bool tally_face_should_move_back(tally_state_t *state) {
bool tally_face_loop(movement_event_t event, void *context) {
tally_state_t *state = (tally_state_t *)context;
static bool using_led = false;
static int8_t beep_sequence[] = {
0, 2,
BUZZER_NOTE_REST, 3,
0, 2,
0
};
if (using_led) {
if(!HAL_GPIO_BTN_MODE_read() && !HAL_GPIO_BTN_LIGHT_read() && !HAL_GPIO_BTN_ALARM_read())
@@ -148,9 +154,11 @@ bool tally_face_loop(movement_event_t event, void *context) {
state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index
_init_val = true;
//play a reset tune
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30);
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30);
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
if (movement_button_should_sound()) {
beep_sequence[0] = BUZZER_NOTE_G6;
beep_sequence[4] = BUZZER_NOTE_E6;
movement_play_sequence(beep_sequence, 0);
}
print_tally(state, movement_button_should_sound());
}
break;
@@ -168,9 +176,11 @@ bool tally_face_loop(movement_event_t event, void *context) {
if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){
state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE();
state->tally_idx = _tally_default[state->tally_default_idx];
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30);
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30);
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30);
if (movement_button_should_sound()) {
beep_sequence[0] = BUZZER_NOTE_E6;
beep_sequence[4] = BUZZER_NOTE_G6;
movement_play_sequence(beep_sequence, 0);
}
print_tally(state, movement_button_should_sound());
}
else{
+3 -2
View File
@@ -280,6 +280,9 @@ void tarot_face_setup(uint8_t watch_face_index, void ** context_ptr) {
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(tarot_state_t));
memset(*context_ptr, 0, sizeof(tarot_state_t));
tarot_state_t *state = (tarot_state_t *)*context_ptr;
state->major_arcana_only = true;
state->num_cards_to_draw = 3;
}
// Emulator only: Seed random number generator
#if __EMSCRIPTEN__
@@ -292,8 +295,6 @@ void tarot_face_activate(void *context) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "Tarot", "TA");
init_deck(state);
state->num_cards_to_draw = 3;
state->major_arcana_only = true;
}
bool tarot_face_loop(movement_event_t event, void *context) {
+186
View File
@@ -0,0 +1,186 @@
/*
* MIT License
*
* Copyright (c) 2026 Wesley Ellis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include <string.h>
#include "tomato_face.h"
#include "watch.h"
#include "watch_utility.h"
static const uint8_t focus_min = 25;
static const uint8_t break_min = 5;
static uint8_t get_length(tomato_state_t *state) {
if (state->kind == tomato_focus) {
return focus_min;
} else {
return break_min;
}
}
static void tomato_start(tomato_state_t *state) {
uint8_t length = get_length(state);
state->mode = tomato_run;
state->now_ts = movement_get_utc_timestamp();
state->target_ts = watch_utility_offset_timestamp(state->now_ts, 0, length, 0);
watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(state->target_ts, 0);
movement_schedule_background_task_for_face(state->watch_face_index, target_dt);
watch_set_indicator(WATCH_INDICATOR_BELL);
}
static void tomato_draw(tomato_state_t *state) {
char buf[16];
uint32_t delta;
div_t result;
uint8_t min = 0;
uint8_t sec = 0;
char kind;
if (state->kind == tomato_break) {
kind = 'b';
} else {
kind = 'f';
}
switch (state->mode) {
case tomato_run:
if (state->target_ts <= state->now_ts)
delta = 0;
else
delta = state->target_ts - state->now_ts;
result = div(delta, 60);
min = result.quot;
sec = result.rem;
break;
case tomato_ready:
min = get_length(state);
sec = 0;
break;
}
sprintf(buf, " %c", kind);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
sprintf(buf, "%2d%02d%2d", min, sec, state->done_count);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
static void tomato_reset(tomato_state_t *state) {
state->mode = tomato_ready;
movement_cancel_background_task_for_face(state->watch_face_index);
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
static void tomato_ring(tomato_state_t *state) {
movement_play_signal();
tomato_reset(state);
if (state->kind == tomato_focus) {
state->kind = tomato_break;
state->done_count++;
} else {
state->kind = tomato_focus;
}
}
void tomato_face_setup(uint8_t watch_face_index, void ** context_ptr) {
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(tomato_state_t));
tomato_state_t *state = (tomato_state_t *)*context_ptr;
memset(*context_ptr, 0, sizeof(tomato_state_t));
state->mode = tomato_ready;
state->kind = tomato_focus;
state->done_count = 0;
state->watch_face_index = watch_face_index;
}
}
void tomato_face_activate(void *context) {
tomato_state_t *state = (tomato_state_t *)context;
if (state->mode == tomato_run) {
state->now_ts = movement_get_utc_timestamp();
watch_set_indicator(WATCH_INDICATOR_BELL);
}
watch_set_colon();
}
bool tomato_face_loop(movement_event_t event, void *context) {
tomato_state_t *state = (tomato_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
watch_display_text_with_fallback(WATCH_POSITION_TOP, "TOMATO", "TO");
tomato_draw(state);
break;
case EVENT_TICK:
if (state->mode == tomato_run) {
state->now_ts++;
}
tomato_draw(state);
break;
case EVENT_LIGHT_BUTTON_DOWN:
movement_illuminate_led();
if (state->mode == tomato_ready) {
if (state->kind == tomato_break) {
state->kind = tomato_focus;
} else {
state->kind = tomato_break;
}
}
tomato_draw(state);
break;
case EVENT_ALARM_BUTTON_UP:
switch(state->mode) {
case tomato_run:
tomato_reset(state);
break;
case tomato_ready:
tomato_start(state);
break;
}
tomato_draw(state);
break;
case EVENT_ALARM_LONG_PRESS:
state->done_count = 0;
break;
case EVENT_BACKGROUND_TASK:
tomato_ring(state);
tomato_draw(state);
break;
case EVENT_TIMEOUT:
if (state->mode != tomato_run) {
movement_move_to_face(0);
}
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void tomato_face_resign(void *context) {
(void) context;
}
+82
View File
@@ -0,0 +1,82 @@
/*
* MIT License
*
* Copyright (c) 2026 Wesley Ellis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef TOMATO_FACE_H_
#define TOMATO_FACE_H_
/*
* TOMATO TIMER face
*
* Add a "tomato" timer watch face that alternates between 25 and 5 minute
* timers as in the Pomodoro Technique.
* https://en.wikipedia.org/wiki/Pomodoro_Technique
*
* The top right letter shows mode (f for focus or b for break).
* The bottom right shows how many focus sessions you've completed.
* (You can reset the count with a long press of alarm)
*
* When you show up and it says 25 minutes, you can start it (alarm),
* switch to 5 minute (light) mode or leave (mode).
*
* When it's running you can reset (alarm), or leave (mode).
*
* When it's done, we beep and go back to step 1, changing switching
* mode from focus to break (or break to focus)
*/
#include "movement.h"
typedef enum {
tomato_ready,
tomato_run,
} tomato_mode;
typedef enum {
tomato_break,
tomato_focus,
} tomato_kind;
typedef struct {
uint32_t target_ts;
uint32_t now_ts;
tomato_mode mode;
tomato_kind kind;
uint8_t done_count;
uint8_t watch_face_index;
} tomato_state_t;
void tomato_face_setup(uint8_t watch_face_index, void ** context_ptr);
void tomato_face_activate(void *context);
bool tomato_face_loop(movement_event_t event, void *context);
void tomato_face_resign(void *context);
#define tomato_face ((const watch_face_t){ \
tomato_face_setup, \
tomato_face_activate, \
tomato_face_loop, \
tomato_face_resign, \
NULL, \
})
#endif // TOMATO_FACE_H_
+1 -2
View File
@@ -36,7 +36,6 @@
#include <string.h>
#include "totp_face.h"
#include "watch.h"
#include "watch_utility.h"
#include "TOTP.h"
#include "base32.h"
@@ -159,7 +158,7 @@ static void totp_generate_and_display(totp_state_t *totp_state) {
}
static inline uint32_t totp_compute_base_timestamp() {
return watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0);
return movement_get_utc_timestamp();
}
void totp_face_setup(uint8_t watch_face_index, void ** context_ptr) {
+1 -2
View File
@@ -30,7 +30,6 @@
#include "base32.h"
#include "watch.h"
#include "watch_utility.h"
#include "filesystem.h"
#include "totp_lfs_face.h"
@@ -253,7 +252,7 @@ void totp_lfs_face_activate(void *context) {
}
#endif
totp_state->timestamp = watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0);
totp_state->timestamp = movement_get_utc_timestamp();
totp_face_set_record(totp_state, 0);
}
+23 -13
View File
@@ -1,17 +1,27 @@
/*
The displayed Japanese Era can be changed by the buttons on the watch, making it also usable as a converter between the Gregorian calendar and the Japanese Era.
Light button: Subtract one year from the Japanese Era.
Start/Stop button: Add one year to the Japanese Era.
Button operations support long-press functionality.
Japanese Era Notations:
r : REIWA (令和)
h : HEISEI (平成)
s : SHOWA(昭和)
*/
* MIT License
*
* Copyright (c) 2025 kbc-yam
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit
* persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdlib.h>
#include <string.h>
+41
View File
@@ -1,6 +1,47 @@
/*
* MIT License
*
* Copyright (c) 2025 kbc-yam
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit
* persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WAREKI_FACE_H_
#define WAREKI_FACE_H_
/*
Display Japanese era names (Wareki)
The displayed Japanese Era can be changed by the buttons on the watch, making it also usable as a converter between the Gregorian calendar and the Japanese Era.
Light button: Subtract one year from the Japanese Era.
Start/Stop button: Add one year to the Japanese Era.
Button operations support long-press functionality.
Japanese Era Notations:
r : REIWA (令和)
h : HEISEI (平成)
s : SHOWA(昭和)
*/
#include "movement.h"
#define REIWA_LIMIT 2018 + 99
+8 -7
View File
@@ -256,7 +256,7 @@ static void reset_board(wordle_state_t *state) {
static void display_title(wordle_state_t *state) {
state->curr_screen = WORDLE_SCREEN_TITLE;
watch_display_text(WATCH_POSITION_TOP_LEFT, "WO");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, "WordLE");
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
@@ -286,7 +286,7 @@ static void display_streak(wordle_state_t *state) {
#else
sprintf(buf, "St%4d", state->streak);
#endif
watch_display_text(WATCH_POSITION_TOP_LEFT, "WO");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, buf);
watch_set_colon();
@@ -304,7 +304,7 @@ static void display_wait(wordle_state_t *state) {
else { // Streak too long to display in top-right
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
}
watch_display_text(WATCH_POSITION_TOP_LEFT, "WO");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO");
watch_display_text(WATCH_POSITION_BOTTOM, " WaIt ");
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
}
@@ -321,15 +321,17 @@ static uint32_t get_day_unix_time(void) {
static void display_lose(wordle_state_t *state, uint8_t subsecond) {
char buf[10];
sprintf(buf," %s", subsecond % 2 ? _valid_words[state->curr_answer] : " ");
watch_display_text(WATCH_POSITION_TOP, "L ");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text_with_fallback(WATCH_POSITION_TOP, "LOSE", "L ");
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
static void display_win(wordle_state_t *state, uint8_t subsecond) {
(void) state;
char buf[10];
sprintf(buf," %s ", subsecond % 2 ? "NICE" : "JOb ");
watch_display_text(WATCH_POSITION_TOP, "W ");
sprintf(buf," %s ", subsecond % 2 ? "NICE" : "JOb ");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "W ");
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
@@ -644,4 +646,3 @@ bool wordle_face_loop(movement_event_t event, void *context) {
void wordle_face_resign(void *context) {
(void) context;
}
+204
View File
@@ -0,0 +1,204 @@
/*
* MIT License
*
* Copyright (c) 2025 Alessandro Genova
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include <string.h>
#include "rtccount_face.h"
#include "watch.h"
#include "sam.h"
#include "watch_utility.h"
#include "watch_common_display.h"
#include "watch_rtc.h"
typedef enum {
RTCCOUNT_STATUS_COUNTER = 0,
RTCCOUNT_STATUS_COUNTER_SUB,
RTCCOUNT_STATUS_MINUTES,
RTCCOUNT_STATUS_MINUTES_DIFF,
RTCCOUNT_STATUS_NUMBER
} rtccount_face_status_t;
typedef struct {
rtccount_face_status_t status;
uint8_t frequency;
uint32_t n_top_of_minute;
uint32_t ref_timestamp;
} rtccount_state_t;
static const uint32_t COUNTER_MASK = (1 << 19) - 1;
static void _rtccount_face_display_string(char* string, uint8_t pos) {
// watch_display_string is deprecated, but there is no alternative for this use-case
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
watch_display_string(string, pos);
#pragma GCC diagnostic pop
}
static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) {
uint32_t counter = watch_rtc_get_counter();
char buf[11] = " 000000\0";
switch (state->status) {
case RTCCOUNT_STATUS_COUNTER: {
buf[0] = 'C';
break;
}
case RTCCOUNT_STATUS_COUNTER_SUB: {
buf[0] = 'S';
break;
}
case RTCCOUNT_STATUS_MINUTES: {
buf[0] = 'M';
break;
}
case RTCCOUNT_STATUS_MINUTES_DIFF: {
buf[0] = 'D';
break;
}
default:
break;
}
_rtccount_face_display_string(buf, 0);
snprintf(buf, sizeof(buf), "%u", event.subsecond);
uint32_t len = strlen(buf);
_rtccount_face_display_string(buf, 4 - len);
switch (state->status) {
case RTCCOUNT_STATUS_COUNTER: {
snprintf(buf, sizeof(buf), "%lu", counter & COUNTER_MASK);
size_t len = strlen(buf);
_rtccount_face_display_string(buf, 10 - len);
break;
}
case RTCCOUNT_STATUS_COUNTER_SUB: {
snprintf(buf, sizeof(buf), "%lu", counter & 127);
size_t len = strlen(buf);
_rtccount_face_display_string(buf, 10 - len);
break;
}
case RTCCOUNT_STATUS_MINUTES: {
snprintf(buf, sizeof(buf), "%lu", state->n_top_of_minute & COUNTER_MASK);
size_t len = strlen(buf);
_rtccount_face_display_string(buf, 10 - len);
break;
}
case RTCCOUNT_STATUS_MINUTES_DIFF: {
uint32_t elapsed_minutes = (movement_get_utc_timestamp() - state->ref_timestamp) / 60;
snprintf(buf, sizeof(buf), "%lu", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK);
size_t len = strlen(buf);
_rtccount_face_display_string(buf, 10 - len);
break;
}
default:
break;
}
}
void rtccount_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(rtccount_state_t));
memset(*context_ptr, 0, sizeof(rtccount_state_t));
rtccount_state_t *state = (rtccount_state_t *) *context_ptr;
state->status = RTCCOUNT_STATUS_COUNTER;
state->frequency = 1;
state->n_top_of_minute = 0;
rtc_date_time_t datetime = movement_get_utc_date_time();
state->ref_timestamp = movement_get_utc_timestamp() - datetime.unit.second;
}
}
void rtccount_face_activate(void *context) {
rtccount_state_t* state = (rtccount_state_t*)context;
movement_request_tick_frequency(state->frequency);
}
bool rtccount_face_loop(movement_event_t event, void *context) {
rtccount_state_t* state = (rtccount_state_t*)context;
switch (event.event_type) {
case EVENT_BACKGROUND_TASK:
state->n_top_of_minute += 1;
break;
case EVENT_ALARM_BUTTON_UP:
if (state->frequency == 128) {
state->frequency = 1;
} else {
state->frequency *= 2;
}
movement_request_tick_frequency(state->frequency);
break;
case EVENT_ALARM_LONG_PRESS:
state->n_top_of_minute = 0;
rtc_date_time_t datetime = movement_get_utc_date_time();
state->ref_timestamp = movement_get_utc_timestamp() - datetime.unit.second;
break;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = (state->status + 1) % RTCCOUNT_STATUS_NUMBER;
_rtccount_face_draw(event, state);
break;
case EVENT_ACTIVATE:
case EVENT_TICK:
_rtccount_face_draw(event, state);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void rtccount_face_resign(void *context) {
(void) context;
movement_request_tick_frequency(1);
}
movement_watch_face_advisory_t rtccount_face_advise(void *context) {
(void) context;
movement_watch_face_advisory_t retval = { 0 };
retval.wants_background_task = true;
return retval;
}
+47
View File
@@ -0,0 +1,47 @@
/*
* MIT License
*
* Copyright (c) 2025 Alessandro Genova
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
/*
* RTCCOUNT FACE
*
* A test face to inspect some metrics of the rtc-counter32 mode.
*/
#include "movement.h"
void rtccount_face_setup(uint8_t watch_face_index, void ** context_ptr);
void rtccount_face_activate(void *context);
bool rtccount_face_loop(movement_event_t event, void *context);
void rtccount_face_resign(void *context);
movement_watch_face_advisory_t rtccount_face_advise(void *context);
#define rtccount_face ((const watch_face_t){ \
rtccount_face_setup, \
rtccount_face_activate, \
rtccount_face_loop, \
rtccount_face_resign, \
rtccount_face_advise, \
})
+55 -113
View File
@@ -47,9 +47,6 @@ typedef struct {
// Selected program
chirpy_demo_program_t program;
// Helps us handle 1/64 ticks during transmission; including countdown timer
chirpy_tick_state_t tick_state;
// Used by chirpy encoder during transmission
chirpy_encoder_state_t encoder_state;
@@ -150,46 +147,10 @@ static void _cdf_update_lcd(chirpy_demo_state_t *state) {
}
}
static void _cdf_quit_chirping(chirpy_demo_state_t *state) {
state->mode = CDM_CHOOSE;
watch_set_buzzer_off();
watch_clear_indicator(WATCH_INDICATOR_BELL);
movement_request_tick_frequency(1);
}
static void _cdf_scale_tick(void *context) {
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
chirpy_tick_state_t *tick_state = &state->tick_state;
// Scale goes in 200Hz increments from 700 Hz to 12.3 kHz -> 58 steps
if (tick_state->seq_pos == 58) {
_cdf_quit_chirping(state);
return;
}
uint32_t freq = 700 + tick_state->seq_pos * 200;
uint32_t period = 1000000 / freq;
watch_set_buzzer_period_and_duty_cycle(period, 25);
watch_set_buzzer_on();
++tick_state->seq_pos;
}
static void _cdf_data_tick(void *context) {
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
uint8_t tone = chirpy_get_next_tone(&state->encoder_state);
// Transmission over?
if (tone == 255) {
_cdf_quit_chirping(state);
return;
}
uint16_t period = chirpy_get_tone_period(tone);
watch_set_buzzer_period_and_duty_cycle(period, 25);
watch_set_buzzer_on();
}
static uint8_t *curr_data_ptr;
static uint16_t curr_data_ix;
static uint16_t curr_data_len;
static chirpy_demo_state_t *curr_state;
static uint8_t _cdf_get_next_byte(uint8_t *next_byte) {
if (curr_data_ix == curr_data_len)
@@ -199,59 +160,60 @@ static uint8_t _cdf_get_next_byte(uint8_t *next_byte) {
return 1;
}
static void _cdf_countdown_tick(void *context) {
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
chirpy_tick_state_t *tick_state = &state->tick_state;
// Countdown over: start actual broadcast
if (tick_state->seq_pos == 8 * 3) {
tick_state->tick_compare = 3;
tick_state->tick_count = -1;
tick_state->seq_pos = 0;
// We'll be chirping out a scale
if (false) { // state->program == CDP_CLEAR) {
tick_state->tick_fun = _cdf_scale_tick;
}
// We'll be chirping out data
else {
// Set up the encoder
chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte);
tick_state->tick_fun = _cdf_data_tick;
// Set up the data
curr_data_ix = 0;
if (state->program == CDP_INFO_SHORT) {
curr_data_ptr = short_data;
curr_data_len = short_data_len;
} else if (state->program == CDP_INFO_LONG) {
curr_data_ptr = long_data_str;
curr_data_len = strlen((const char *)long_data_str);
} else if (state->program == CDP_INFO_NANOSEC) {
curr_data_ptr = activity_buffer;
curr_data_len = activity_buffer_size;
}
}
return;
static void _cdf_on_chirping_done(void) {
if (curr_state) {
curr_state->mode = CDM_CHOOSE;
}
// Sound or turn off buzzer
if ((tick_state->seq_pos % 8) == 0) {
watch_set_buzzer_period_and_duty_cycle(NotePeriods[BUZZER_NOTE_A5], 25);
watch_set_buzzer_on();
} else if ((tick_state->seq_pos % 8) == 1) {
watch_set_buzzer_off();
}
++tick_state->seq_pos;
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
static void _cdm_setup_chirp(chirpy_demo_state_t *state) {
// We want frequent callbacks from now on
movement_request_tick_frequency(64);
static bool _cdm_raw_source_fn(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration) {
// Beep countdown
if (position < 6) {
if (position % 2) {
*period = WATCH_BUZZER_PERIOD_REST;
*duration = 56;
} else {
*period = NotePeriods[BUZZER_NOTE_A5];
*duration = 8;
}
return false;
}
chirpy_demo_state_t *state = (chirpy_demo_state_t *)userdata;
uint8_t tone = chirpy_get_next_tone(&state->encoder_state);
// Transmission over?
if (tone == 255) {
return true;
}
*period = chirpy_get_tone_period(tone);
*duration = 3;
return false;
}
static void _cdm_start_transmission(chirpy_demo_state_t *state) {
watch_set_indicator(WATCH_INDICATOR_BELL);
state->mode = CDM_CHIRPING;
// Set up tick state; start with countdown
state->tick_state.tick_count = -1;
state->tick_state.tick_compare = 8;
state->tick_state.seq_pos = 0;
state->tick_state.tick_fun = _cdf_countdown_tick;
// Set up the data
curr_state = state;
curr_data_ix = 0;
if (state->program == CDP_INFO_SHORT) {
curr_data_ptr = short_data;
curr_data_len = short_data_len;
} else if (state->program == CDP_INFO_LONG) {
curr_data_ptr = long_data_str;
curr_data_len = strlen((const char *)long_data_str);
} else if (state->program == CDP_INFO_NANOSEC) {
curr_data_ptr = activity_buffer;
curr_data_len = activity_buffer_size;
}
chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte);
watch_buzzer_play_raw_source(_cdm_raw_source_fn, state, _cdf_on_chirping_done);
}
bool chirpy_demo_face_loop(movement_event_t event, void *context) {
@@ -261,12 +223,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
case EVENT_ACTIVATE:
_cdf_update_lcd(state);
break;
case EVENT_MODE_BUTTON_UP:
// Do not exit face while we're chirping
if (state->mode != CDM_CHIRPING) {
movement_move_to_next_face();
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
case EVENT_LIGHT_BUTTON_UP:
// We don't do light.
break;
@@ -286,10 +243,6 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
state->program = CDP_CLEAR;
_cdf_update_lcd(state);
}
// If chirping: stoppit
else if (state->mode == CDM_CHIRPING) {
_cdf_quit_chirping(state);
}
break;
case EVENT_ALARM_LONG_PRESS:
// If in choose mode: start chirping
@@ -299,16 +252,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
movement_force_led_off();
movement_move_to_next_face();
} else {
_cdm_setup_chirp(state);
}
}
break;
case EVENT_TICK:
if (state->mode == CDM_CHIRPING) {
++state->tick_state.tick_count;
if (state->tick_state.tick_count == state->tick_state.tick_compare) {
state->tick_state.tick_count = 0;
state->tick_state.tick_fun(context);
_cdm_start_transmission(state);
}
}
break;
@@ -317,15 +261,13 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
if (state->mode != CDM_CHIRPING) {
movement_move_to_face(0);
}
// fall through
default:
movement_default_loop_handler(event);
break;
}
// Return true if the watch can enter standby mode. False needed when chirping.
if (state->mode == CDM_CHIRPING)
return false;
else
return true;
return true;
}
void chirpy_demo_face_resign(void *context) {
@@ -86,6 +86,13 @@ void activity_logging_face_activate(void *context) {
bool activity_logging_face_loop(movement_event_t event, void *context) {
activity_logging_state_t *state = (activity_logging_state_t *)context;
switch (event.event_type) {
case EVENT_LIGHT_LONG_PRESS:
movement_illuminate_led();
break;
case EVENT_LIGHT_BUTTON_DOWN:
state->display_index = (state->display_index + ACTIVITY_LOGGING_NUM_DAYS - 1) % ACTIVITY_LOGGING_NUM_DAYS;
_activity_logging_face_update_display(state);
break;
case EVENT_ALARM_BUTTON_DOWN:
state->display_index = (state->display_index + 1) % ACTIVITY_LOGGING_NUM_DAYS;
// fall through
@@ -40,6 +40,8 @@
*
* A short press of the Alarm button moves backwards in the data log, showing yesterday's active minutes,
* then the day before, etc. going back 14 days.
* A short press of the Light button moves forward in the data log, looping around if we're on the most-recent day.
* Holding the Light button will illuminate the display.
*
*/
+1 -1
View File
@@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state)
lis2dw_fifo_t fifo;
float x = 0, y = 0, z = 0;
lis2dw_read_fifo(&fifo);
lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT / DISPLAY_FREQUENCY);
if (fifo.count == 0) {
return;
}
+7 -16
View File
@@ -27,7 +27,6 @@
#include <math.h>
#include "finetune_face.h"
#include "nanosec_face.h"
#include "watch_utility.h"
#include "delay.h"
extern nanosec_state_t nanosec_state;
@@ -51,7 +50,7 @@ void finetune_face_activate(void *context) {
}
static float finetune_get_hours_passed(void) {
uint32_t current_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0);
uint32_t current_time = movement_get_utc_timestamp();
return (current_time - nanosec_state.last_correction_time) / 3600.0f;
}
@@ -64,7 +63,7 @@ static void finetune_update_display(void) {
if (finetune_page == 0) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "FTU", "FT");
watch_date_time_t date_time = watch_rtc_get_date_time();
watch_date_time_t date_time = movement_get_utc_date_time();
sprintf(buf, "%04d%02d", abs(total_adjustment), date_time.unit.second);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
@@ -106,17 +105,9 @@ static void finetune_adjust_subseconds(int delta) {
watch_rtc_enable(false);
delay_ms(delta);
if (delta > 500) {
watch_date_time_t date_time = watch_rtc_get_date_time();
date_time.unit.second = (date_time.unit.second + 1) % 60;
if (date_time.unit.second == 0) { // Overflow
date_time.unit.minute = (date_time.unit.minute + 1) % 60;
if (date_time.unit.minute == 0) { // Overflow
date_time.unit.hour = (date_time.unit.hour + 1) % 24;
if (date_time.unit.hour == 0) // Overflow
date_time.unit.day++;
}
}
watch_rtc_set_date_time(date_time);
uint32_t timestamp = movement_get_utc_timestamp();
timestamp += 1;
movement_set_utc_timestamp(timestamp);
}
watch_rtc_enable(true);
}
@@ -126,7 +117,7 @@ static void finetune_update_correction_time(void) {
nanosec_state.freq_correction += roundf(nanosec_get_aging() * 100);
// Remember when we last corrected time
nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0);
nanosec_state.last_correction_time = movement_get_utc_timestamp();
nanosec_save();
movement_move_to_face(0); // Go to main face after saving settings
}
@@ -146,7 +137,7 @@ bool finetune_face_loop(movement_event_t event, void *context) {
// We flash green LED once per minute to measure clock error, when we are not on first screen
if (finetune_page!=0) {
watch_date_time_t date_time;
date_time = watch_rtc_get_date_time();
date_time = movement_get_utc_date_time();
if (date_time.unit.second == 0) {
watch_set_led_green();
#ifndef __EMSCRIPTEN__
-5
View File
@@ -44,11 +44,6 @@
* worry about aging only on second/third years of watch calibration (if you
* are really looking at less than 10 seconds per year of error).
*
* Warning, do not use at the first second of a month, as you might stay at
* the same month and it will surprise you. Just wait 1 second...We are not
* fully replicating RTC timer behavior when RTC is off.
* Simulating months and years is... too much complexity.
*
* For full usage instructions, please refer to the wiki:
* https://www.sensorwatch.net/docs/watchfaces/nanosec/
*/
+4 -6
View File
@@ -27,7 +27,6 @@
#include <math.h>
#include "nanosec_face.h"
#include "filesystem.h"
#include "watch_utility.h"
int16_t freq_correction_residual = 0; // Dithering 0.1ppm correction, does not need to be configured.
int16_t freq_correction_previous = -30000;
@@ -44,8 +43,7 @@ const float voltage_coefficient = 0.241666667 * dithering; // 10 * ppm/V. Nomina
static void nanosec_init_profile(void) {
nanosec_changed = true;
nanosec_state.correction_cadence = 10;
watch_date_time_t date_time = watch_rtc_get_date_time();
nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(date_time, 0);
nanosec_state.last_correction_time = movement_get_utc_timestamp();
// init data after changing profile - do that once per profile selection
switch (nanosec_state.correction_profile) {
@@ -265,8 +263,8 @@ static void nanosec_next_edit_screen(void) {
float nanosec_get_aging() // Returns aging correction in ppm
{
watch_date_time_t date_time = watch_rtc_get_date_time();
float years = (watch_utility_date_time_to_unix_time(date_time, 0) - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune
uint32_t timestamp = movement_get_utc_timestamp();
float years = (timestamp - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune
return years*nanosec_state.aging_ppm_pa/100.0f;
}
@@ -377,7 +375,7 @@ movement_watch_face_advisory_t nanosec_face_advise(void *context) {
// No need for background correction if we are on profile 0 - static hardware correction.
if (nanosec_state.correction_profile != 0) {
watch_date_time_t date_time = watch_rtc_get_date_time();
watch_date_time_t date_time = movement_get_utc_date_time();
retval.wants_background_task = date_time.unit.minute % nanosec_state.correction_cadence == 0;
}
+4 -6
View File
@@ -46,7 +46,7 @@ static void _handle_alarm_button(watch_date_time_t date_time, uint8_t current_pa
current_offset = movement_get_current_timezone_offset_for_zone(movement_get_timezone_index());
return;
case 0: // year
date_time.unit.year = ((date_time.unit.year % 60) + 1);
date_time.unit.year = (date_time.unit.year + 1) % 60;
break;
case 1: // month
date_time.unit.month = (date_time.unit.month % 12) + 1;
@@ -91,6 +91,8 @@ bool set_time_face_loop(movement_event_t event, void *context) {
watch_date_time_t date_time = movement_get_local_date_time();
switch (event.event_type) {
case EVENT_ACTIVATE:
break;
case EVENT_TICK:
if (_quick_ticks_running) {
if (HAL_GPIO_BTN_ALARM_read()) _handle_alarm_button(date_time, current_page);
@@ -106,10 +108,6 @@ bool set_time_face_loop(movement_event_t event, void *context) {
case EVENT_ALARM_LONG_UP:
_abort_quick_ticks();
break;
case EVENT_MODE_BUTTON_UP:
_abort_quick_ticks();
movement_move_to_next_face();
return false;
case EVENT_LIGHT_BUTTON_DOWN:
current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS;
*((uint8_t *)context) = current_page;
@@ -185,6 +183,6 @@ bool set_time_face_loop(movement_event_t event, void *context) {
void set_time_face_resign(void *context) {
(void) context;
watch_set_led_off();
movement_store_settings();
movement_request_tick_frequency(1);
}
+67 -3
View File
@@ -77,6 +77,64 @@ static void beep_setting_advance(void) {
}
}
static void signal_setting_display(uint8_t subsecond) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SIG", "SI");
watch_display_text(WATCH_POSITION_BOTTOM, "SIGNAL");
if (subsecond % 2) {
if (movement_signal_volume() == WATCH_BUZZER_VOLUME_LOUD) {
// H for HIGH
watch_display_text(WATCH_POSITION_TOP_RIGHT, " H");
}
else {
// L for LOW
watch_display_text(WATCH_POSITION_TOP_RIGHT, " L");
}
}
}
static void signal_setting_advance(void) {
if (movement_signal_volume() == WATCH_BUZZER_VOLUME_SOFT) {
// was soft. make it loud.
movement_set_signal_volume(WATCH_BUZZER_VOLUME_LOUD);
} else {
// was loud. make it soft.
movement_set_signal_volume(WATCH_BUZZER_VOLUME_SOFT);
}
signal_setting_display(1);
movement_play_signal();
}
static void alarm_setting_display(uint8_t subsecond) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "ALM", "AL");
watch_display_text(WATCH_POSITION_BOTTOM, "ALARM ");
if (subsecond % 2) {
if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_LOUD) {
// H for HIGH
watch_display_text(WATCH_POSITION_TOP_RIGHT, " H");
}
else {
// L for LOW
watch_display_text(WATCH_POSITION_TOP_RIGHT, " L");
}
}
}
static void alarm_setting_advance(void) {
if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_SOFT) {
// was soft. make it loud.
movement_set_alarm_volume(WATCH_BUZZER_VOLUME_LOUD);
} else {
// was loud. make it soft.
movement_set_alarm_volume(WATCH_BUZZER_VOLUME_SOFT);
}
alarm_setting_display(1);
movement_play_alarm();
}
static void timeout_setting_display(uint8_t subsecond) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "TMOUt", "TO");
if (subsecond % 2) {
@@ -235,7 +293,7 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) {
settings_state_t *state = (settings_state_t *)*context_ptr;
int8_t current_setting = 0;
state->num_settings = 5; // baseline, without LED settings
state->num_settings = 7; // baseline, without LED settings
#ifdef BUILD_GIT_HASH
state->num_settings++;
#endif
@@ -256,6 +314,12 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) {
state->settings_screens[current_setting].display = beep_setting_display;
state->settings_screens[current_setting].advance = beep_setting_advance;
current_setting++;
state->settings_screens[current_setting].display = signal_setting_display;
state->settings_screens[current_setting].advance = signal_setting_advance;
current_setting++;
state->settings_screens[current_setting].display = alarm_setting_display;
state->settings_screens[current_setting].advance = alarm_setting_advance;
current_setting++;
state->settings_screens[current_setting].display = timeout_setting_display;
state->settings_screens[current_setting].advance = timeout_setting_advance;
current_setting++;
@@ -322,7 +386,7 @@ bool settings_face_loop(movement_event_t event, void *context) {
case EVENT_MODE_BUTTON_UP:
movement_force_led_off();
movement_move_to_next_face();
return false;
return true;
case EVENT_ALARM_BUTTON_UP:
state->settings_screens[state->current_page].advance();
break;
@@ -339,7 +403,7 @@ bool settings_face_loop(movement_event_t event, void *context) {
movement_force_led_on(color.red | color.red << 4,
color.green | color.green << 4,
color.blue | color.blue << 4);
return false;
return true;
} else {
movement_force_led_off();
return true;
+6
View File
@@ -44,6 +44,12 @@
* This setting allows you to choose whether the Mode button should emit
* a beep when pressed, and if so, how loud it should be. Options are
* "Y" for yes and "N" for no.
*
* SI / SIG - Signal beep.
* This setting allows you to choose the hourly chime buzzer volume.
*
* AL / ALM - Alarm beep.
* This setting allows you to choose the alarm buzzer volume.
*
* TO / Tmout - Timeout.
* Sets the time until screens that time out (like Settings and Time Set)