diff options
| -rw-r--r-- | movement/make/Makefile | 1 | ||||
| -rw-r--r-- | movement/movement_faces.h | 1 | ||||
| -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 | 
4 files changed, 523 insertions, 0 deletions
diff --git a/movement/make/Makefile b/movement/make/Makefile index 60084594..9def50de 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -75,6 +75,7 @@ SRCS += \    ../watch_faces/demo/frequency_correction_face.c \    ../watch_faces/complication/alarm_face.c \    ../watch_faces/complication/ratemeter_face.c \ +  ../watch_faces/complication/rpn_calculator_alt_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. diff --git a/movement/movement_faces.h b/movement/movement_faces.h index 9333401b..98d0a5c5 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -59,6 +59,7 @@  #include "frequency_correction_face.h"  #include "alarm_face.h"  #include "ratemeter_face.h" +#include "rpn_calculator_alt_face.h"  #include "weeknumber_clock_face.h"  // New includes go above this line. 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_ +  | 
