summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--movement/make/Makefile1
-rw-r--r--movement/movement_faces.h1
-rw-r--r--movement/watch_faces/complication/rpn_calculator_alt_face.c459
-rw-r--r--movement/watch_faces/complication/rpn_calculator_alt_face.h62
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_
+