Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Balanced nonary clock face #347

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions movement/make/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ SRCS += \
../watch_faces/clock/minute_repeater_decimal_face.c \
../watch_faces/complication/tuning_tones_face.c \
../watch_faces/complication/kitchen_conversions_face.c \
../watch_faces/clock/nonary_clock_face.c \
# New watch faces go above this line.

# Leave this line at the bottom of the file; it has all the targets for making your project.
Expand Down
1 change: 1 addition & 0 deletions movement/movement_faces.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
#include "minute_repeater_decimal_face.h"
#include "tuning_tones_face.h"
#include "kitchen_conversions_face.h"
#include "nonary_clock_face.h"
// New includes go above this line.

#endif // MOVEMENT_FACES_H_
4 changes: 2 additions & 2 deletions movement/watch_faces/clock/clock_face.c
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ static bool clock_display_some(watch_date_time current, watch_date_time previous
if ((current.reg >> 6) == (previous.reg >> 6)) {
// everything before seconds is the same, don't waste cycles setting those segments.

watch_display_character_lp_seconds('0' + current.unit.second / 10, 8);
watch_display_character_lp_seconds('0' + current.unit.second % 10, 9);
watch_display_character_lp('0' + current.unit.second / 10, 8);
watch_display_character_lp('0' + current.unit.second % 10, 9);

return true;

Expand Down
4 changes: 2 additions & 2 deletions movement/watch_faces/clock/minute_repeater_decimal_face.c
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ bool minute_repeater_decimal_face_loop(movement_event_t event, movement_settings

if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
// everything before seconds is the same, don't waste cycles setting those segments.
watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8);
watch_display_character_lp_seconds('0' + date_time.unit.second % 10, 9);
watch_display_character_lp('0' + date_time.unit.second / 10, 8);
watch_display_character_lp('0' + date_time.unit.second % 10, 9);
break;
} else if ((date_time.reg >> 12) == (previous_date_time >> 12) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
// everything before minutes is the same.
Expand Down
236 changes: 236 additions & 0 deletions movement/watch_faces/clock/nonary_clock_face.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*
* MIT License
*
* Copyright (c) 2024 James Haggerty <[email protected]>
*
* 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 "nonary_clock_face.h"
#include "watch.h"
#include "watch_utility.h"
#include "watch_private_display.h"

static const int TICK_FREQUENCY = 8;

static void _update_alarm_indicator(bool settings_alarm_enabled, nonary_clock_state_t *state) {
state->alarm_enabled = settings_alarm_enabled;
if (state->alarm_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
}

void nonary_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
(void) settings;
(void) watch_face_index;

if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(nonary_clock_state_t));
nonary_clock_state_t *state = (nonary_clock_state_t *)*context_ptr;
state->signal_enabled = false;
state->watch_face_index = watch_face_index;
}
}

static const int32_t NONARY_SECS_IN_HOUR = 9 * 9 * 9 * 9;

/*
* Bit position -> segment mapping.
* --0--
* | |
* 5 1
* | |
* --6--
* | |
* 4 2
* | |
* --3--
*
* Digits (-4,-3,-2-1,0,1,2,3,4)
* __ __ __ __ __
* __ __ | | __ __ __|
* |__|, __|,__|,__,|__|, |, |,| |,| |
*/
uint8_t nonary_digit_map[] = {
0b01011101, // -4
0b01001101, // -3
0b00001101, // -2
0b00001001, // -1
0b00111111, // 0
0b00000100, // 1
0b01000100, // 2
0b01010100, // 3
0b01010110, // 4
};

static uint8_t previous_display[10];

static void display_nonary(int32_t val, int pos, int max_len) {
int sign = val >= 0 ? 1 : -1;
val = abs(val);
for (int i = 0; i < max_len && pos >= 0; ++i) {
int digit = val % 9;
val /= 9;
if (digit >= 5) {
digit -= 9;
val += 1;
}
uint8_t c = nonary_digit_map[digit * sign + 4];
if (previous_display[pos] != c) {
watch_display_segdata(c, pos--);
}
}
}

static int32_t find_nonary_secs_from_hour(watch_date_time date_time, int ticks) {
int32_t nonary_secs_past_hour = ((date_time.unit.minute * 60 + date_time.unit.second) * TICK_FREQUENCY + ticks) * NONARY_SECS_IN_HOUR / TICK_FREQUENCY / 3600;

return nonary_secs_past_hour <= NONARY_SECS_IN_HOUR / 2 ? nonary_secs_past_hour : (nonary_secs_past_hour - NONARY_SECS_IN_HOUR);
}

void nonary_clock_face_activate(movement_settings_t *settings, void *context) {
nonary_clock_state_t *state = (nonary_clock_state_t *)context;

if (watch_tick_animation_is_running()) watch_stop_tick_animation();

if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);

// handle chime indicator
if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
else watch_clear_indicator(WATCH_INDICATOR_BELL);

// show alarm indicator if there is an active alarm
_update_alarm_indicator(settings->bit.alarm_enabled, state);

watch_set_colon();

// this ensures that none of the timestamp fields will match, so we can re-render them all.
state->previous_date_time.reg = 0xFFFFFFFF;

// Since our seconds aren't really seconds, we need a higher tick frequency
// to avoid skips and jerky time changes.
movement_request_tick_frequency(TICK_FREQUENCY);

memset(previous_display, ' ', sizeof(previous_display));
}

bool nonary_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
static uint8_t ticks = 0;
static int32_t previous_nonary_secs_from_hour = 0;
nonary_clock_state_t *state = (nonary_clock_state_t *)context;

watch_date_time date_time;
watch_date_time previous_date_time;
switch (event.event_type) {
case EVENT_ACTIVATE:
case EVENT_TICK:
case EVENT_LOW_ENERGY_UPDATE:
date_time = watch_rtc_get_date_time();
previous_date_time = state->previous_date_time;
state->previous_date_time = date_time;

if (event.event_type == EVENT_TICK) {
if (previous_date_time.reg == date_time.reg) {
++ticks;
} else {
ticks = 0;
}
}

// check the battery voltage once a day...
if (date_time.unit.day != state->last_battery_check) {
state->last_battery_check = date_time.unit.day;
watch_enable_adc();
uint16_t voltage = watch_get_vcc_voltage();
watch_disable_adc();
// 2.2 volts will happen when the battery has maybe 5-10% remaining?
// we can refine this later.
state->battery_low = (voltage < 2200);
}

// ...and set the LAP indicator if low.
if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP);

// handle alarm indicator
if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state);

int32_t nonary_secs_from_hour = find_nonary_secs_from_hour(date_time, ticks);
if (previous_nonary_secs_from_hour == nonary_secs_from_hour) {
break;
}
previous_nonary_secs_from_hour = nonary_secs_from_hour;
display_nonary(nonary_secs_from_hour, 9, 4);

int32_t nonary_hour = (int32_t)date_time.unit.hour - 12 + (nonary_secs_from_hour < 0);
display_nonary(nonary_hour, 5, 2);

if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
watch_display_character_lp(' ', 8);
watch_display_character_lp(' ', 9);
if (!watch_tick_animation_is_running()) watch_start_tick_animation(500);
}

if (date_time.unit.day != previous_date_time.unit.day) {
char buf[5];
sprintf(buf, "%s%2d", watch_utility_get_weekday(date_time), date_time.unit.day);
watch_display_string(buf, 0);
}

break;
case EVENT_ALARM_LONG_PRESS:
state->signal_enabled = !state->signal_enabled;
if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
else watch_clear_indicator(WATCH_INDICATOR_BELL);
break;
case EVENT_BACKGROUND_TASK:
// uncomment this line to snap back to the clock face when the hour signal sounds:
// movement_move_to_face(state->watch_face_index);
#ifdef SIGNAL_TUNE_DEFAULT
movement_play_signal();
#else
//movement_play_tune();
#endif
break;
default:
return movement_default_loop_handler(event, settings);
}

return true;
}

void nonary_clock_face_resign(movement_settings_t *settings, void *context) {
(void) settings;
(void) context;

movement_request_tick_frequency(1);
}

bool nonary_clock_face_wants_background_task(movement_settings_t *settings, void *context) {
(void) settings;
nonary_clock_state_t *state = (nonary_clock_state_t *)context;
if (!state->signal_enabled) return false;

watch_date_time date_time = watch_rtc_get_date_time();

// TODO to make this work better, we need to have the CLOCK->COUNT32 change
// (so we can set COMP to an appropriate interval).
// At the moment, this will not fire at appropriate times.
return date_time.unit.minute == 0;
}
101 changes: 101 additions & 0 deletions movement/watch_faces/clock/nonary_clock_face.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* MIT License
*
* Copyright (c) 2024 James Haggerty <[email protected]>
*
* 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 NONARY_CLOCK_FACE_H_
#define NONARY_CLOCK_FACE_H_

/*
* Balanced Nonary Clock Face
*
* Like simple clock face, but... confusing?
*
* What's the point of having a customisable wrist watch unless you have bizarre timing schemes?
* This maintains the normal concept of hours, but divides each hour into
* 9*9*9*9 seconds, so that we can display the time in nonary.
*
* But not just any nonary, this is _balanced_ nonary, such that our digit
* values are -4, -3, -2, -1, 0, 1, 2, 3, 4
*
* Midday is therefore 00:00:00. An example of counting before midday
* and after:
*
* 00:00:0(-4)
* 00:00:0(-3)
* 00:00:0(-2)
* 00:00:0(-1)
* 00:00:00
* 00:00:01
* 00:00:02
* 00:00:03
* 00:00:04
* 00:00:1(-4)
* 00:00:1(-3)
* 00:00:1(-2)
* 00:00:1(-1)
* 00:00:10
* 00:00:11
* etc.
*
* This has the advantage that you can always see what hour/minute you're closer to.
*
* But wait - how can we display the negative digits? Well, to do this
* we use the top 'bar' in a digit as the negative sign, and for those
* numbers compress them into the bottom 4 segments, and then simply use
* the number of other segments filled as the digit. So, counting
* from -4 to 4:
* __ __ __ __ __
* __ __ | | __ __ __|
* |__|, __|,__|,__,|__|, |, |,| |,| |
*
* Sadly, because the top and the bottom segments are tied, we can't
* easily use the same digit for positive and negative values without
* running into 'normal' numbers in confusing ways.
*/

#include "movement.h"

typedef struct {
watch_date_time previous_date_time;
uint8_t last_battery_check;
uint8_t watch_face_index;
bool signal_enabled;
bool battery_low;
bool alarm_enabled;
} nonary_clock_state_t;

void nonary_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
void nonary_clock_face_activate(movement_settings_t *settings, void *context);
bool nonary_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
void nonary_clock_face_resign(movement_settings_t *settings, void *context);
bool nonary_clock_face_wants_background_task(movement_settings_t *settings, void *context);

#define nonary_clock_face ((const watch_face_t){ \
nonary_clock_face_setup, \
nonary_clock_face_activate, \
nonary_clock_face_loop, \
nonary_clock_face_resign, \
nonary_clock_face_wants_background_task, \
})

#endif
4 changes: 2 additions & 2 deletions movement/watch_faces/clock/repetition_minute_face.c
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ bool repetition_minute_face_loop(movement_event_t event, movement_settings_t *se

if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
// everything before seconds is the same, don't waste cycles setting those segments.
watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8);
watch_display_character_lp_seconds('0' + date_time.unit.second % 10, 9);
watch_display_character_lp('0' + date_time.unit.second / 10, 8);
watch_display_character_lp('0' + date_time.unit.second % 10, 9);
break;
} else if ((date_time.reg >> 12) == (previous_date_time >> 12) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
// everything before minutes is the same.
Expand Down
4 changes: 2 additions & 2 deletions movement/watch_faces/clock/simple_clock_bin_led_face.c
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ bool simple_clock_bin_led_face_loop(movement_event_t event, movement_settings_t

if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
// everything before seconds is the same, don't waste cycles setting those segments.
watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8);
watch_display_character_lp_seconds('0' + date_time.unit.second % 10, 9);
watch_display_character_lp('0' + date_time.unit.second / 10, 8);
watch_display_character_lp('0' + date_time.unit.second % 10, 9);
break;
} else if ((date_time.reg >> 12) == (previous_date_time >> 12) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
// everything before minutes is the same.
Expand Down
Loading
Loading