diff options
Diffstat (limited to 'movement/watch_faces')
-rw-r--r-- | movement/watch_faces/complication/rpn_calculator_alt_face.c | 459 | ||||
-rw-r--r-- | movement/watch_faces/complication/rpn_calculator_alt_face.h | 62 |
2 files changed, 521 insertions, 0 deletions
diff --git a/movement/watch_faces/complication/rpn_calculator_alt_face.c b/movement/watch_faces/complication/rpn_calculator_alt_face.c new file mode 100644 index 00000000..bfbce902 --- /dev/null +++ b/movement/watch_faces/complication/rpn_calculator_alt_face.c @@ -0,0 +1,459 @@ +/* + * MIT License + * + * Copyright (c) 2022 James Haggerty + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* RPN Calculator alternate face. + * + * Operations appear in the 'day' section; ALARM changes between operations when operation is flashing. + * LIGHT executes current operation. + * + * This is the alternate face because it has a non-traditional number entry system which + * I call 'guess a number'. In number entry mode, the watch tries to guess which number you + * want, and you respond with 'smaller' (left - MODE) or larger (right - ALARM). This means + * that when you _are_ entering a number, MODE will no longer move between faces! + * + * Example of entering the number 27 + * - select the NO operation (probably unnecessary, as this is the default), + * and execute it by hitting LIGHT. + * - you are now in number entry mode; you know this because nothing is flashing. + * - Watch displays 10; you hit ALARM to say you want a larger number. + * - Watch displays 100; you hit MODE to say you want a smaller number. + * - Continuing: 50 -> MODE -> 30 -> MODE -> 20 -> ALARM -> 27 + * - Hit LIGHT to add the number to the stack (and now 'NO' is flashing + * again, indicating you're back in operation selection mode). + * + * One other thing to watch out for is how quickly it will switch into scientific notation + * due to the limitations of the display when you have large numbers or non-integer values. + * In this mode, the 'colon' serves at the decimal point, and the numbers in the top right + * are the exponent. + * + * As with the main movement firmware, this has the concept of 'secondary' functions which + * you can jump to by a long hold of ALARM on NO. These are functions to do with stack + * manipulation (pop, swap, dupe, clear, size (le)). If you're _not_ on NO, a long + * hold will take you back to it. + * + * See 'functions' below for names of all operations. + */ + +#include <stdlib.h> +#include <string.h> +#include <math.h> + +#include "rpn_calculator_alt_face.h" + +static void show_fn(calculator_state_t *state, uint8_t subsecond); + +void rpn_calculator_alt_face_setup(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(calculator_state_t)); + memset(*context_ptr, 0, sizeof(calculator_state_t)); + // Do any one-time tasks in here; the inside of this conditional happens only at boot. + } + // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. +} + +static void show_number(double num) { + char buf[9] = {0}; + bool negative = num < 0; + int max_digits = negative ? 5 : 6; + + // Add back in for debugging... + // printf("%f\n", num); + + if (isnan(num)) { + watch_clear_colon(); + watch_display_string(" nan ", 2); + return; + } + + if (negative) { + num = -num; + } + + // Can we reasonably represent this number without a decimal point? + if (num == 0 || (num >= 0.5 && fabs(num - (int)num) < 0.0001)) { + if (floor(log10(num)) + 1 <= max_digits) { + if (negative) { + sprintf(buf, " -%-5d", (int)round(num)); + } else { + sprintf(buf, " %-6d", (int)round(num)); + } + watch_clear_colon(); + watch_display_string(buf, 2); + return; + } + } + + // Is this a floating point number where scientific + // notation won't get us much? (i.e. between 0.1 and 1) + if (num < 1 && num >= 0.0999) { + // Display as boring floating point number... (e.g. 0.25) + int digits = (int)round(num * 10000); + sprintf(buf, " 0%04d", digits); + if (negative) { + buf[2 ] = '-'; + } + watch_set_colon(); + watch_display_string(buf, 2); + return; + } + + // Fall back to scientific notation + + // Calculate exponent + int exponent = 0; + while (num < 1) { + num *= 10; + --exponent; + } + + while (num >= 10) { + num /= 10; + ++exponent; + } + + if (exponent < -9) { + sprintf(buf, " small "); + watch_clear_colon(); + watch_display_string(buf, 2); + return; + } + + if (exponent > 39) { + sprintf(buf, " big "); + watch_clear_colon(); + watch_display_string(buf, 2); + return; + } + + sprintf(buf, "%2d%c%05d", exponent, negative ? '-' : ' ', (int)round(num * 10000)); + watch_set_colon(); + watch_display_string(buf, 2); +} + +#define C (s->stack[s->stack_size - 1]) +#define PUSH(x) (s->stack[++s->stack_size - 1] = x) +#define POP() (s->stack[s->stack_size-- - 1]) + +void rpn_calculator_alt_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + calculator_state_t *s = (calculator_state_t *)context; + s->min = s->max = NAN; +} + +static void change_mode(calculator_state_t *s, enum calculator_mode mode) { + s->mode = mode; + s->fn_index = 0; + show_fn(s, 0); + // Faster tick in operation mode so we can blink. + movement_request_tick_frequency(mode == CALC_OPERATION ? 4 : 1); +} + +// Binary-ish search to find the right number. direction is +1 if it should be bigger, -1 if it should be smaller. +static void adjust_number(calculator_state_t *s, int direction) { + if (direction > 0) { + s->min = C; + } else { + s->max = C; + } + + // If the direction we want to go has no bound (i.e. isnan), + // then first get the sign right (moving to 0, then +-10), and + // after than go up by *10. + if (isnan(direction > 0 ? s->max : s->min)) { + if (direction * C < 0) { + C = 0; + } else if (C == 0) { + C = direction * 10; + } else { + C *= 10; + } + } else { + // We have a higher and lower bound. Split them. + C = (s->max + s->min) / 2; + // Subtract 0.1 so we don't apply most significant rounding to things that are _exactly_ 1/10/100 apart. + double mag = log10(fabs(s->max - s->min)) - 0.1; + if (mag > 0.0) { + // i.e. the different is >= 2, which means we want to round aggressively + // to not show people complicated looking numbers. + // e.g. this takes a number like 3.2 to 3, or a number like 464 to 500 + // (depending on how fine-grained 'mag' tells us to be). + float div = pow(10, floor(mag)); + int sign = C < 0 ? -1 : 1; + C = sign * floor(fabs(C) / div) * div; + } + } +} + +static void fn_number(calculator_state_t *s) { + PUSH(10); + s->min = s->max = NAN; + change_mode(s, CALC_NUMBER); +} + +static void fn_add(calculator_state_t *s) { + double a = POP(); + double b = POP(); + PUSH(a + b); +} + +static void fn_sub(calculator_state_t *s) { + double a = POP(); + double b = POP(); + PUSH(b - a); +} + +static void fn_mul(calculator_state_t *s) { + double a = POP(); + double b = POP(); + PUSH(a * b); +} + +static void fn_div(calculator_state_t *s) { + double a = POP(); + double b = POP(); + PUSH(b / a); +} + +static void fn_pow(calculator_state_t *s) { + double a = POP(); + double b = POP(); + PUSH(pow(b, a)); +} + +static void fn_sqrt(calculator_state_t *s) { + double x = POP(); + PUSH(sqrt(x)); +} + +static void fn_log(calculator_state_t *s) { + double x = POP(); + PUSH(log(x)); +} + +static void fn_log10(calculator_state_t *s) { + double x = POP(); + PUSH(log10(x)); +} + +static void fn_e(calculator_state_t *s) { + PUSH(M_E); +} + +static void fn_sin(calculator_state_t *s) { + double x = POP(); + PUSH(sin(x)); +} + +static void fn_cos(calculator_state_t *s) { + double x = POP(); + PUSH(cos(x)); +} + +static void fn_tan(calculator_state_t *s) { + double x = POP(); + PUSH(tan(x)); +} + +static void fn_pi(calculator_state_t *s) { + PUSH(M_PI); +} + +static void fn_pop(calculator_state_t *s) { + --s->stack_size; +} + +static void fn_swap(calculator_state_t *s) { + double a = POP(); + double b = POP(); + PUSH(a); + PUSH(b); +} + +static void fn_duplicate(calculator_state_t *s) { + double a = POP(); + PUSH(a); + PUSH(a); +} + +static void fn_clear(calculator_state_t *s) { + s->stack_size = 0; +} + +static void fn_size(calculator_state_t *s) { + double a = s->stack_size; + PUSH(a); +} + +struct { + char name[2]; + uint8_t input; + uint8_t output; + void (*func)(calculator_state_t *); +} functions[] = { + {{'n', 'o'}, 0, 1, fn_number}, + {{'*', ' '}, 2, 1, fn_add}, // First position * actually looks like a '+'. + {{'-', ' '}, 2, 1, fn_sub}, + {{'H', ' '}, 2, 1, fn_mul}, // For actual *, we throw in the middle vertical segment onto the H. + {{'/', ' '}, 2, 1, fn_div}, // There's also some minor hackery on '/'. + {{'P', 'o'}, 2, 1, fn_pow}, + {{'S', 'r'}, 1, 1, fn_sqrt}, + {{'L', 'n'}, 1, 1, fn_log}, + {{'L', 'o'}, 1, 1, fn_log10}, + {{'e', ' '}, 0, 1, fn_e}, + {{'P', 'i'}, 0, 1, fn_pi}, + {{'C', 'o'}, 1, 1, fn_cos}, + {{'S', 'i'}, 1, 1, fn_sin}, + {{'T', 'a'}, 1, 1, fn_tan}, + // Stack operations. Accessible via secondary_fn_index (i.e. alarm long press). + {{'P', 'O'}, 1, 0, fn_pop}, // This ends up displaying the same as 'POW'. But at least it's in a different place. + {{'S', 'W'}, 2, 2, fn_swap}, + {{'d', 'u'}, 1, 1, fn_duplicate}, // Uppercase 'D' is a bit too 'O' for me. + {{'C', 'L'}, 1, 0, fn_clear}, // Operation lie - takes _everything_ off the stack, but a check of 1 is sufficient. + {{'L', 'E'}, 1, 0, fn_size}, +}; + +#define FUNCTIONS_LEN (sizeof(functions) / sizeof(functions[0])) +#define SECONDARY_FN_INDEX (FUNCTIONS_LEN - 4) + +// Show the function name (using day display) +static void show_fn(calculator_state_t *s, uint8_t subsecond) { + if (subsecond % 2) { + // blink + watch_display_string(" ", 0); + return; + } + + char *name = functions[s->fn_index].name; + char buf[3] = {name[0], name[1], '\0'}; + watch_display_string(buf, 0); + // The first position has a bunch of segments, and I have minor + // disagreements with the character set choices in watch_display_string, + // so we tweak a little here. + switch (buf[0]) { + case 'H': + // Use the middle segment lines to make our 'H' a '*'-ish thing. + watch_set_pixel(1, 14); + break; + case '/': + // Add a middle bar to division. + watch_set_pixel(1, 15); + break; + default: + break; + } +} + +// Show the top of the stack (using everything except day display). +static void show_stack_top(calculator_state_t *s) { + if (s->stack_size > 0) { + show_number(C); + } else { + watch_display_string(" ------", 2); + watch_clear_colon(); + } +} + +bool rpn_calculator_alt_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + calculator_state_t *s = (calculator_state_t *)context; + (void) settings; + + int proposed_stack_size; + + switch (event.event_type) { + case EVENT_ACTIVATE: + change_mode(s, CALC_OPERATION); + show_stack_top(s); + break; + case EVENT_TICK: + if (s->mode == CALC_OPERATION) { + show_fn(s, event.subsecond); + } + break; + case EVENT_MODE_BUTTON_UP: + if (s->mode == CALC_NUMBER) { + adjust_number(s, -1); + show_stack_top(s); + } else { + movement_move_to_next_face(); + return false; + } + break; + case EVENT_LIGHT_BUTTON_UP: + proposed_stack_size = s->stack_size - functions[s->fn_index].input; + + if (s->mode == CALC_NUMBER) { + change_mode(s, CALC_OPERATION); + } else if (proposed_stack_size < 0 || proposed_stack_size + functions[s->fn_index].output > CALC_MAX_STACK_SIZE) { + movement_play_signal(); + break; + } else { + functions[s->fn_index].func(s); + show_stack_top(s); + s->fn_index = 0; + show_fn(s, 0); + } + + break; + case EVENT_ALARM_BUTTON_UP: + if (s->mode == CALC_NUMBER) { + adjust_number(s, 1); + show_stack_top(s); + } else { + s->fn_index = (s->fn_index + 1) % FUNCTIONS_LEN; + show_fn(s, 0); + } + break; + case EVENT_ALARM_LONG_PRESS: + if (s->mode == CALC_OPERATION) { + if (s->fn_index == 0) { + s->fn_index = SECONDARY_FN_INDEX; + } else { + s->fn_index = 0; + } + show_fn(s, 0); + } + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + case EVENT_LOW_ENERGY_UPDATE: + default: + break; + } + + // return true if the watch can enter standby mode. If you are PWM'ing an LED or buzzing the buzzer here, + // you should return false since the PWM driver does not operate in standby mode. + return true; +} + +void rpn_calculator_alt_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + + // handle any cleanup before your watch face goes off-screen. +} + diff --git a/movement/watch_faces/complication/rpn_calculator_alt_face.h b/movement/watch_faces/complication/rpn_calculator_alt_face.h new file mode 100644 index 00000000..2a964675 --- /dev/null +++ b/movement/watch_faces/complication/rpn_calculator_alt_face.h @@ -0,0 +1,62 @@ +/* + * MIT License + * + * Copyright (c) 2022 <#author_name#> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef CALCULATOR_FACE_H_ +#define CALCULATOR_FACE_H_ + +#include "movement.h" + +#define CALC_MAX_STACK_SIZE 20 + +enum calculator_mode { + CALC_OPERATION = 0, + CALC_NUMBER, +}; + +typedef struct { + double stack[CALC_MAX_STACK_SIZE]; + uint8_t stack_size; // this is the current stack top + 1 (so that '0' means nothing on the stack) + uint8_t fn_index; + + double min; + double max; + + enum calculator_mode mode; +} calculator_state_t; + +void rpn_calculator_alt_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void rpn_calculator_alt_face_activate(movement_settings_t *settings, void *context); +bool rpn_calculator_alt_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void rpn_calculator_alt_face_resign(movement_settings_t *settings, void *context); + +#define rpn_calculator_alt_face ((const watch_face_t){ \ + rpn_calculator_alt_face_setup, \ + rpn_calculator_alt_face_activate, \ + rpn_calculator_alt_face_loop, \ + rpn_calculator_alt_face_resign, \ + NULL, \ +}) + +#endif // CALCULATOR_FACE_H_ + |