/* * 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 #include #include #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. }