Merge branch 'main' into tide_face
This commit is contained in:
@@ -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();
|
||||
}
|
||||
@@ -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, \
|
||||
})
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Executable
+472
@@ -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();
|
||||
}
|
||||
}
|
||||
Executable
+96
@@ -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_
|
||||
@@ -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 */
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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_
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 ( );
|
||||
}
|
||||
@@ -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_
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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, \
|
||||
})
|
||||
@@ -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.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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/
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user