]> git.earman.xyz Git - sensor-watch.git/commitdiff
Update Kitchen Conversions Face for Second Movement (#24)
authorPrimmR <primmr@icloud.com>
Sun, 6 Jul 2025 15:47:12 +0000 (16:47 +0100)
committerGitHub <noreply@github.com>
Sun, 6 Jul 2025 15:47:12 +0000 (11:47 -0400)
* Basic second movement support

* Switch alarm and light button functionality

legacy/watch_faces/complication/kitchen_conversions_face.c [deleted file]
legacy/watch_faces/complication/kitchen_conversions_face.h [deleted file]
movement_faces.h
watch-faces.mk
watch-faces/complication/kitchen_conversions_face.c [new file with mode: 0644]
watch-faces/complication/kitchen_conversions_face.h [new file with mode: 0644]

diff --git a/legacy/watch_faces/complication/kitchen_conversions_face.c b/legacy/watch_faces/complication/kitchen_conversions_face.c
deleted file mode 100644 (file)
index 5c1d1dd..0000000
+++ /dev/null
@@ -1,477 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2023 PrimmR
- *
- * 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 "kitchen_conversions_face.h"
-
-typedef struct
-{
-    char name[6];          // Name to display on selection
-    double conv_factor_uk; // Unit as represented in base units (UK)
-    double conv_factor_us; // Unit as represented in base units (US)
-    int16_t linear_factor; // Addition of constant (For temperatures)
-} unit;
-
-#define TICK_FREQ 4
-
-#define MEASURES_COUNT 3 // Number of different measurement 'types'
-#define WEIGHT 0
-#define TEMP 1
-#define VOL 2
-
-// Names of measurements
-static char measures[MEASURES_COUNT][6] = {"WeIght", " Temp", " VOL"};
-
-// Number of items in each category
-#define WEIGHT_COUNT 4
-#define TEMP_COUNT 3
-#define VOL_COUNT 9
-const uint8_t units_count[4] = {WEIGHT_COUNT, TEMP_COUNT, VOL_COUNT};
-
-static const unit weights[WEIGHT_COUNT] = {
-    {" g", 1., 1., 0}, // BASE
-    {" kg", 1000., 1000, 0},
-    {"Ounce", 28.34952, 28.34952, 0},
-    {" Pound", 453.5924, 453.5924, 0},
-};
-
-static const unit temps[TEMP_COUNT] = {
-    {" # C", 1.8, 1.8, 32},
-    {" # F", 1., 1., 0}, // BASE
-    {"Gas Mk", 25., 25., 250},
-};
-
-static const unit vols[VOL_COUNT] = {
-    {"  n&L", 1., 1., 0}, // BASE (ml)
-    {"   L", 1000., 1000., 0},
-    {" Fl Oz", 28.41306, 29.57353, 0},
-    {" Tbsp", 17.75816, 14.78677, 0},
-    {" Tsp", 5.919388, 4.928922, 0},
-    {"  Cup", 284.1306, 236.5882, 0},
-    {" Pint", 568.2612, 473.1765, 0},
-    {" Quart", 1136.522, 946.353, 0},
-    {"Gallon", 4546.09, 3785.412, 0},
-};
-
-static int8_t calc_success_seq[5] = {BUZZER_NOTE_G6, 10, BUZZER_NOTE_C7, 10, 0};
-static int8_t calc_fail_seq[5] = {BUZZER_NOTE_C7, 10, BUZZER_NOTE_G6, 10, 0};
-
-// Resets all state variables to 0
-static void reset_state(kitchen_conversions_state_t *state)
-{
-    state->pg = measurement;
-    state->measurement_i = 0;
-    state->from_i = 0;
-    state->from_is_us = movement_use_imperial_units(); // If uses imperial, most likely to be US
-    state->to_i = 0;
-    state->to_is_us = movement_use_imperial_units();
-    state->selection_value = 0;
-    state->selection_index = 0;
-    state->light_held = false;
-}
-
-void kitchen_conversions_face_setup(uint8_t watch_face_index, void **context_ptr)
-{
-    (void)watch_face_index;
-    if (*context_ptr == NULL)
-    {
-        *context_ptr = malloc(sizeof(kitchen_conversions_state_t));
-        memset(*context_ptr, 0, sizeof(kitchen_conversions_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.
-}
-
-void kitchen_conversions_face_activate(void *context)
-{
-    kitchen_conversions_state_t *state = (kitchen_conversions_state_t *)context;
-
-    // Handle any tasks related to your watch face coming on screen.
-    movement_request_tick_frequency(TICK_FREQ);
-
-    reset_state(state, settings);
-}
-
-// Increments index pointer by 1, wrapping
-#define increment_wrapping(index, wrap) ({(index)++; index %= wrap; })
-
-static uint32_t pow_10(uint8_t n)
-{
-    uint32_t result = 1;
-    for (int i = 0; i < n; i++)
-    {
-        result *= 10;
-    }
-    return result;
-}
-
-// Returns correct list of units for the measurement index
-static unit *get_unit_list(uint8_t measurement_i)
-{
-    switch (measurement_i)
-    {
-    case WEIGHT:
-        return (unit *)weights;
-
-    case TEMP:
-        return (unit *)temps;
-
-    case VOL:
-        return (unit *)vols;
-
-    default:
-        return (unit *)weights;
-    }
-}
-
-// Increment digit by 1 in input (wraps)
-static void increment_input(kitchen_conversions_state_t *state)
-{
-    uint8_t digit = state->selection_value / pow_10(DISPLAY_DIGITS - 1 - state->selection_index) % 10;
-    if (digit != 9)
-    {
-        state->selection_value += pow_10(DISPLAY_DIGITS - 1 - state->selection_index);
-    }
-    else
-    {
-        state->selection_value -= 9 * pow_10(DISPLAY_DIGITS - 1 - state->selection_index);
-    }
-}
-
-// Displays the list of units in the selected category
-static void display_units(uint8_t measurement_i, uint8_t list_i)
-{
-    watch_display_string(get_unit_list(measurement_i)[list_i].name, 4);
-}
-
-static void display(kitchen_conversions_state_t *state, uint8_t subsec)
-{
-    watch_clear_display();
-
-    switch (state->pg)
-    {
-    case measurement:
-    {
-        watch_display_string("Un", 0);
-        watch_display_string(measures[state->measurement_i], 4);
-    }
-    break;
-
-    case from:
-        display_units(state->measurement_i, state->from_i);
-
-        // Display Fr if non-locale specific, else display locale and F
-        if (state->measurement_i == VOL)
-        {
-            watch_display_string("F", 3);
-
-            char *locale = state->from_is_us ? "A " : "GB";
-            watch_display_string(locale, 0);
-        }
-        else
-        {
-            watch_display_string("Fr", 0);
-        }
-
-        break;
-
-    case to:
-        display_units(state->measurement_i, state->to_i);
-
-        // Display To if non-locale specific, else display locale and T
-        if (state->measurement_i == VOL)
-        {
-            watch_display_string("T", 3);
-
-            char *locale = state->to_is_us ? "A " : "GB";
-            watch_display_string(locale, 0);
-        }
-        else
-        {
-            watch_display_string("To", 0);
-        }
-
-        break;
-
-    case input:
-    {
-        char buf[7];
-        sprintf(buf, "%06lu", state->selection_value);
-        watch_display_string(buf, 4);
-
-        // Only allow ints for Gas Mk
-        if (state->measurement_i == TEMP && state->from_i == 2)
-        {
-            watch_display_string("  ", 8);
-        }
-
-        // Blink digit (on & off) twice a second
-        if (subsec % 2)
-        {
-            watch_display_string(" ", 4 + state->selection_index);
-        }
-
-        watch_display_string("In", 0);
-    }
-    break;
-
-    case result:
-    {
-        unit froms = get_unit_list(state->measurement_i)[state->from_i];
-        unit tos = get_unit_list(state->measurement_i)[state->to_i];
-        // Chooses correct factor for locale
-        double f_conv_factor = state->from_is_us ? froms.conv_factor_us : froms.conv_factor_uk;
-        double t_conv_factor = state->to_is_us ? tos.conv_factor_us : tos.conv_factor_uk;
-        // Converts
-        double to_base = (state->selection_value * f_conv_factor) + 100 * froms.linear_factor;
-        double conversion = ((to_base - 100 * tos.linear_factor) / t_conv_factor);
-
-        // If number too large or too small
-        uint8_t lower_bound = (state->measurement_i == TEMP && state->to_i == 2) ? 100 : 0;
-        if (conversion >= 1000000 || conversion < lower_bound)
-        {
-            watch_set_indicator(WATCH_INDICATOR_BELL);
-            watch_display_string("Err", 5);
-
-            if (movement_button_should_sound())
-                watch_buzzer_play_sequence(calc_fail_seq, NULL);
-        }
-        else
-        {
-            uint32_t rounded = conversion + .5;
-            char buf[7];
-            sprintf(buf, "%6lu", rounded);
-            watch_display_string(buf, 4);
-
-            // Make sure LSDs always filled
-            if (rounded < 10)
-            {
-                watch_display_string("00", 7);
-            }
-            else if (rounded < 100)
-            {
-                watch_display_string("0", 7);
-            }
-
-            if (movement_button_should_sound())
-                watch_buzzer_play_sequence(calc_success_seq, NULL);
-        }
-        watch_display_string("=", 1);
-    }
-
-    break;
-
-    default:
-        break;
-    }
-}
-
-bool kitchen_conversions_face_loop(movement_event_t event, void *context)
-{
-    kitchen_conversions_state_t *state = (kitchen_conversions_state_t *)context;
-
-    switch (event.event_type)
-    {
-    case EVENT_ACTIVATE:
-        // Initial UI
-        display(state, settings, event.subsecond);
-        break;
-    case EVENT_TICK:
-        // Update for blink animation on input
-        if (state->pg == input)
-        {
-            display(state, settings, event.subsecond);
-
-            // Increments input twice a second when light button held
-            if (state->light_held && event.subsecond % 2)
-                increment_input(state);
-        }
-        break;
-    case EVENT_LIGHT_BUTTON_UP:
-        // Cycles options
-        switch (state->pg)
-        {
-        case measurement:
-            increment_wrapping(state->measurement_i, MEASURES_COUNT);
-            break;
-
-        case from:
-            increment_wrapping(state->from_i, units_count[state->measurement_i]);
-            break;
-
-        case to:
-            increment_wrapping(state->to_i, units_count[state->measurement_i]);
-            break;
-
-        case input:
-            increment_input(state);
-            break;
-
-        default:
-            break;
-        }
-
-        // Light button does nothing on final screen
-        if (state->pg != result)
-            display(state, settings, event.subsecond);
-
-        state->light_held = false;
-
-        break;
-
-    case EVENT_ALARM_BUTTON_UP:
-        // Increments selected digit
-        if (state->pg == input)
-        {
-
-            // Moves between digits in input
-            // Wraps at 6 digits unless gas mark selected
-            if (state->selection_index < (DISPLAY_DIGITS - 1) - 2 * (state->measurement_i == TEMP && state->from_i == 2))
-            {
-                state->selection_index++;
-            }
-            else
-            {
-                state->pg++;
-                display(state, settings, event.subsecond);
-            }
-        }
-        // Moves forward 1 page
-        else
-        {
-            if (state->pg == SCREEN_NUM - 1)
-            {
-                reset_state(state, settings);
-            }
-            else
-            {
-                state->pg++;
-            }
-
-            // Play boop
-            if (movement_button_should_sound())
-                watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
-        }
-
-        display(state, settings, event.subsecond);
-
-        state->light_held = false;
-
-        break;
-
-    case EVENT_ALARM_LONG_PRESS:
-        // Moves backwards through pages, resetting certain values
-        if (state->pg != measurement)
-        {
-            switch (state->pg)
-            {
-            case measurement:
-                state->measurement_i = 0;
-                break;
-
-            case from:
-                state->from_i = 0;
-                state->from_is_us = movement_use_imperial_units();
-                break;
-
-            case to:
-                state->to_i = 0;
-                state->to_is_us = movement_use_imperial_units();
-                break;
-
-            case input:
-                state->selection_index = 0;
-                state->selection_value = 0;
-                break;
-
-            case result:
-                state->selection_index = 0;
-                break;
-
-            default:
-                break;
-            }
-
-            state->pg--;
-            display(state, settings, event.subsecond);
-
-            // Play beep
-            if (movement_button_should_sound())
-                watch_buzzer_play_note(BUZZER_NOTE_C8, 50);
-
-            state->light_held = false;
-        }
-        break;
-
-    case EVENT_LIGHT_LONG_PRESS:
-        // Switch between locales
-        if (state->measurement_i == VOL)
-        {
-            if (state->pg == from)
-            {
-                state->from_is_us = !state->from_is_us;
-            }
-            else if (state->pg == to)
-            {
-                state->to_is_us = !state->to_is_us;
-            }
-
-            if (state->pg == from || state->pg == to)
-            {
-                display(state, settings, event.subsecond);
-
-                // Play bleep
-                if (movement_button_should_sound())
-                    watch_buzzer_play_note(BUZZER_NOTE_E7, 50);
-            }
-        }
-
-        // Sets flag to increment input digit when light held
-        if (state->pg == input)
-            state->light_held = true;
-
-        break;
-
-    case EVENT_LIGHT_LONG_UP:
-        state->light_held = false;
-        break;
-
-    case EVENT_TIMEOUT:
-        movement_move_to_face(0);
-        break;
-
-    default:
-        return movement_default_loop_handler(event);
-    }
-
-    return true;
-}
-
-void kitchen_conversions_face_resign(void *context)
-{
-    (void)context;
-
-    // handle any cleanup before your watch face goes off-screen.
-}
diff --git a/legacy/watch_faces/complication/kitchen_conversions_face.h b/legacy/watch_faces/complication/kitchen_conversions_face.h
deleted file mode 100644 (file)
index 33c6cf4..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2023 PrimmR
- *
- * 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 KITCHEN_CONVERSIONS_FACE_H_
-#define KITCHEN_CONVERSIONS_FACE_H_
-
-#include "movement.h"
-
-/*
- * Kitchen Conversions
- * A face that allows the user to convert between common kitchen units of measurement
- *
- * How to use
- * ----------
- * Short press the alarm button to move forward through menus, and long press to move backwards
- *
- * Press the light button to cycle through options in the menus
- *
- * When inputting a number, the light button moves forward one place and the alarm button increments a digit by one
- *
- * To convert between Imperial (GB) and US (A) measurements of volume, hold the light button
- *
- */
-
-#define SCREEN_NUM 5
-
-// Names of each page
-typedef enum
-{
-    measurement,
-    from,
-    to,
-    input,
-    result,
-} page_t;
-
-#define DISPLAY_DIGITS 6
-
-// Settings when app is running
-typedef struct
-{
-    page_t pg;
-    uint8_t measurement_i;
-    uint8_t from_i;
-    bool from_is_us;
-    uint8_t to_i;
-    bool to_is_us;
-    uint32_t selection_value;
-    uint8_t selection_index;
-    bool light_held;
-} kitchen_conversions_state_t;
-
-void kitchen_conversions_face_setup(uint8_t watch_face_index, void **context_ptr);
-void kitchen_conversions_face_activate(void *context);
-bool kitchen_conversions_face_loop(movement_event_t event, void *context);
-void kitchen_conversions_face_resign(void *context);
-
-#define kitchen_conversions_face ((const watch_face_t){ \
-    kitchen_conversions_face_setup,                     \
-    kitchen_conversions_face_activate,                  \
-    kitchen_conversions_face_loop,                      \
-    kitchen_conversions_face_resign,                    \
-    NULL,                                               \
-})
-
-#endif // KITCHEN_CONVERSIONS_FACE_H_
index d2d36e4a86285fa4edfff90c0a1f19ecf5d2a7fe..23ffc44e41b87b2408e1bf8e49598e2af342fa2c 100644 (file)
@@ -51,6 +51,7 @@
 #include "nanosec_face.h"
 #include "mars_time_face.h"
 #include "peek_memory_face.h"
+#include "kitchen_conversions_face.h"
 #include "periodic_table_face.h"
 #include "squash_face.h"
 #include "totp_face.h"
index 2c60f09b674e74a11f3118ce4c9d4dc3887e4078..71a939d94ddbf784cc3769afafa94be2a2702098 100644 (file)
@@ -28,5 +28,6 @@ SRCS += \
   ./watch-faces/settings/nanosec_face.c \
   ./watch-faces/io/chirpy_demo_face.c \
   ./watch-faces/io/irda_upload_face.c \
+  ./watch-faces/complication/kitchen_conversions_face.c \
   ./watch-faces/complication/periodic_table_face.c \
 # New watch faces go above this line.
diff --git a/watch-faces/complication/kitchen_conversions_face.c b/watch-faces/complication/kitchen_conversions_face.c
new file mode 100644 (file)
index 0000000..7428de3
--- /dev/null
@@ -0,0 +1,494 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 PrimmR
+ *
+ * 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 "kitchen_conversions_face.h"
+
+#include "watch_common_display.h"
+
+typedef struct
+{
+    char name[6];          // Name to display on selection
+    double conv_factor_uk; // Unit as represented in base units (UK)
+    double conv_factor_us; // Unit as represented in base units (US)
+    int16_t linear_factor; // Addition of constant (For temperatures)
+} unit;
+
+#define TICK_FREQ 4
+
+#define MEASURES_COUNT 3 // Number of different measurement 'types'
+#define WEIGHT 0
+#define TEMP 1
+#define VOL 2
+
+// Names of measurements (classic & custom LCD)
+static char measures[MEASURES_COUNT][7] = {"n&ass", " Temp", " VOL"};
+static char measures_custom[MEASURES_COUNT][7] = {"n&ass", "  temp", "Volume"};
+
+// Number of items in each category
+#define WEIGHT_COUNT 4
+#define TEMP_COUNT 3
+#define VOL_COUNT 9
+const uint8_t units_count[4] = {WEIGHT_COUNT, TEMP_COUNT, VOL_COUNT};
+
+static const unit weights[WEIGHT_COUNT] = {
+    {" g", 1., 1., 0}, // BASE
+    {" kg", 1000., 1000, 0},
+    {"Ounce", 28.34952, 28.34952, 0},
+    {" Pound", 453.5924, 453.5924, 0},
+};
+
+static const unit temps[TEMP_COUNT] = {
+    {" # C", 1.8, 1.8, 32},
+    {" # F", 1., 1., 0}, // BASE
+    {"Gas Mk", 25., 25., 250},
+};
+
+static const unit temps_custom[TEMP_COUNT] = {
+    {" #C", 1.8, 1.8, 32},
+    {" #F", 1., 1., 0}, // BASE
+    {"Gas Mk", 25., 25., 250},
+};
+
+static const unit vols[VOL_COUNT] = {
+    {"  n&L", 1., 1., 0}, // BASE (ml)
+    {"   L", 1000., 1000., 0},
+    {" Fl Oz", 28.41306, 29.57353, 0},
+    {" Tbsp", 17.75816, 14.78677, 0},
+    {" Tsp", 5.919388, 4.928922, 0},
+    {"  Cup", 284.1306, 236.5882, 0},
+    {" Pint", 568.2612, 473.1765, 0},
+    {" Quart", 1136.522, 946.353, 0},
+    {"Gallon", 4546.09, 3785.412, 0},
+};
+
+static int8_t calc_success_seq[5] = {BUZZER_NOTE_G6, 10, BUZZER_NOTE_C7, 10, 0};
+static int8_t calc_fail_seq[5] = {BUZZER_NOTE_C7, 10, BUZZER_NOTE_G6, 10, 0};
+
+// Resets all state variables to 0
+static void reset_state(kitchen_conversions_state_t *state)
+{
+    state->pg = measurement;
+    state->measurement_i = 0;
+    state->from_i = 0;
+    state->from_is_us = movement_use_imperial_units(); // If uses imperial, most likely to be US
+    state->to_i = 0;
+    state->to_is_us = movement_use_imperial_units();
+    state->selection_value = 0;
+    state->selection_index = 0;
+    state->alarm_held = false;
+}
+
+void kitchen_conversions_face_setup(uint8_t watch_face_index, void **context_ptr)
+{
+    (void)watch_face_index;
+    if (*context_ptr == NULL)
+    {
+        *context_ptr = malloc(sizeof(kitchen_conversions_state_t));
+        memset(*context_ptr, 0, sizeof(kitchen_conversions_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.
+}
+
+void kitchen_conversions_face_activate(void *context)
+{
+    kitchen_conversions_state_t *state = (kitchen_conversions_state_t *)context;
+
+    // Handle any tasks related to your watch face coming on screen.
+    movement_request_tick_frequency(TICK_FREQ);
+
+    reset_state(state);
+}
+
+// Increments index pointer by 1, wrapping
+#define increment_wrapping(index, wrap) ({(index)++; index %= wrap; })
+
+static uint32_t pow_10(uint8_t n)
+{
+    uint32_t result = 1;
+    for (int i = 0; i < n; i++)
+    {
+        result *= 10;
+    }
+    return result;
+}
+
+// Returns correct list of units for the measurement index
+static unit *get_unit_list(uint8_t measurement_i)
+{
+    switch (measurement_i)
+    {
+    case WEIGHT:
+        return (unit *)weights;
+
+    case TEMP:
+        return (unit *)(watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM ? temps_custom : temps);
+
+    case VOL:
+        return (unit *)vols;
+
+    default:
+        return (unit *)weights;
+    }
+}
+
+// Increment digit by 1 in input (wraps)
+static void increment_input(kitchen_conversions_state_t *state)
+{
+    uint8_t digit = state->selection_value / pow_10(DISPLAY_DIGITS - 1 - state->selection_index) % 10;
+    if (digit != 9)
+    {
+        state->selection_value += pow_10(DISPLAY_DIGITS - 1 - state->selection_index);
+    }
+    else
+    {
+        state->selection_value -= 9 * pow_10(DISPLAY_DIGITS - 1 - state->selection_index);
+    }
+}
+
+// Displays the list of units in the selected category
+static void display_units(uint8_t measurement_i, uint8_t list_i)
+{
+    // Slighly hacky way to improve ml display on new LCD
+    if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM && measurement_i == VOL && list_i == 0)
+        watch_display_text(WATCH_POSITION_BOTTOM, "    mL");
+    else
+        watch_display_text(WATCH_POSITION_BOTTOM, get_unit_list(measurement_i)[list_i].name);
+}
+
+static void display(kitchen_conversions_state_t *state, uint8_t subsec)
+{
+    watch_clear_display();
+
+    switch (state->pg)
+    {
+    case measurement:
+    {
+        watch_display_text_with_fallback(WATCH_POSITION_TOP, "Unit", "Un");
+        char* measurement_name = (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM ? measures_custom : measures)[state->measurement_i];
+        watch_display_text(WATCH_POSITION_BOTTOM, measurement_name);
+    }
+    break;
+
+    case from:
+        display_units(state->measurement_i, state->from_i);
+
+        // Display Fr if non-locale specific, else display locale and F
+        if (state->measurement_i == VOL)
+        {
+            watch_display_text(WATCH_POSITION_TOP_RIGHT, " F");
+
+            char *locale_custom = state->from_is_us ? "USA" : "IMP";
+            char *locale = state->from_is_us ? "A " : "GB";
+            watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, locale_custom, locale);
+        }
+        else
+        {
+            watch_display_text_with_fallback(WATCH_POSITION_TOP, "Frm", "Fr");
+        }
+
+        break;
+
+    case to:
+        display_units(state->measurement_i, state->to_i);
+
+        // Display to if non-locale specific, else display locale and 't'
+        if (state->measurement_i == VOL) {
+            watch_display_text(WATCH_POSITION_TOP_RIGHT, " T");
+
+            char *locale_custom = state->to_is_us ? "USA" : "IMP";
+            char *locale = state->to_is_us ? "A " : "GB";
+            watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, locale_custom, locale);
+        }
+        else
+        {
+            watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, " to", "to");
+        }
+
+        break;
+
+    case input:
+    {
+        char buf[7];
+        sprintf(buf, "%06u", state->selection_value);
+        watch_display_text(WATCH_POSITION_BOTTOM, buf);
+
+        // Only allow ints for Gas Mk
+        if (state->measurement_i == TEMP && state->from_i == 2)
+        {
+            watch_display_character(' ', 8);
+            watch_display_character(' ', 9);
+        }
+
+        // Blink digit (on & off) twice a second
+        if (subsec % 2)
+        {
+            watch_display_character(' ', 4 + state->selection_index);
+        }
+
+        watch_display_text_with_fallback(WATCH_POSITION_TOP, "Input", "In");
+    }
+    break;
+
+    case result:
+    {
+        unit froms = get_unit_list(state->measurement_i)[state->from_i];
+        unit tos = get_unit_list(state->measurement_i)[state->to_i];
+        // Chooses correct factor for locale
+        double f_conv_factor = state->from_is_us ? froms.conv_factor_us : froms.conv_factor_uk;
+        double t_conv_factor = state->to_is_us ? tos.conv_factor_us : tos.conv_factor_uk;
+        // Converts
+        double to_base = (state->selection_value * f_conv_factor) + 100 * froms.linear_factor;
+        double conversion = ((to_base - 100 * tos.linear_factor) / t_conv_factor);
+
+        // If number too large or too small
+        uint8_t lower_bound = (state->measurement_i == TEMP && state->to_i == 2) ? 100 : 0;
+        if (conversion >= 1000000 || conversion < lower_bound)
+        {
+            watch_set_indicator(WATCH_INDICATOR_BELL);
+            watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " Error", " Err");
+
+            if (movement_button_should_sound())
+                watch_buzzer_play_sequence(calc_fail_seq, NULL);
+        }
+        else
+        {
+            uint32_t rounded = conversion + .5;
+            char buf[7];
+            sprintf(buf, "%6u", rounded);
+            watch_display_text(WATCH_POSITION_BOTTOM, buf);
+
+            // Make sure LSDs always filled
+            if (rounded < 10)
+            {
+                watch_display_character('0', 7);
+                watch_display_character('0', 8);
+            }
+            else if (rounded < 100)
+            {
+                watch_display_character('0', 7);
+            }
+
+            if (movement_button_should_sound())
+                watch_buzzer_play_sequence(calc_success_seq, NULL);
+        }
+        watch_display_text_with_fallback(WATCH_POSITION_TOP, "Res =", " =");
+    }
+
+    break;
+
+    default:
+        break;
+    }
+}
+
+bool kitchen_conversions_face_loop(movement_event_t event, void *context)
+{
+    kitchen_conversions_state_t *state = (kitchen_conversions_state_t *)context;
+
+    switch (event.event_type)
+    {
+    case EVENT_ACTIVATE:
+        // Initial UI
+        display(state, event.subsecond);
+        break;
+    case EVENT_TICK:
+        // Update for blink animation on input
+        if (state->pg == input)
+        {
+            display(state, event.subsecond);
+
+            // Increments input twice a second when light button held
+            if (state->alarm_held && event.subsecond % 2)
+                increment_input(state);
+        }
+        break;
+    case EVENT_ALARM_BUTTON_UP:
+        // Cycles options
+        switch (state->pg)
+        {
+        case measurement:
+            increment_wrapping(state->measurement_i, MEASURES_COUNT);
+            break;
+
+        case from:
+            increment_wrapping(state->from_i, units_count[state->measurement_i]);
+            break;
+
+        case to:
+            increment_wrapping(state->to_i, units_count[state->measurement_i]);
+            break;
+
+        case input:
+            increment_input(state);
+            break;
+
+        default:
+            break;
+        }
+
+        // Light button does nothing on final screen
+        if (state->pg != result)
+            display(state, event.subsecond);
+
+        state->alarm_held = false;
+
+        break;
+
+    case EVENT_LIGHT_BUTTON_UP:
+        // Increments selected digit
+        if (state->pg == input)
+        {
+
+            // Moves between digits in input
+            // Wraps at 6 digits unless gas mark selected
+            if (state->selection_index < (DISPLAY_DIGITS - 1) - 2 * (state->measurement_i == TEMP && state->from_i == 2))
+            {
+                state->selection_index++;
+            }
+            else
+            {
+                state->pg++;
+                display(state, event.subsecond);
+            }
+        }
+        // Moves forward 1 page
+        else
+        {
+            if (state->pg == SCREEN_NUM - 1)
+            {
+                reset_state(state);
+            }
+            else
+            {
+                state->pg++;
+            }
+
+            // Play boop
+            if (movement_button_should_sound())
+                watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
+        }
+
+        display(state, event.subsecond);
+
+        state->alarm_held = false;
+
+        break;
+
+    case EVENT_LIGHT_LONG_PRESS:
+        // Moves backwards through pages, resetting certain values
+        if (state->pg != measurement)
+        {
+            switch (state->pg)
+            {
+            case measurement:
+                state->measurement_i = 0;
+                break;
+
+            case from:
+                state->from_i = 0;
+                state->from_is_us = movement_use_imperial_units();
+                break;
+
+            case to:
+                state->to_i = 0;
+                state->to_is_us = movement_use_imperial_units();
+                break;
+
+            case input:
+                state->selection_index = 0;
+                state->selection_value = 0;
+                break;
+
+            case result:
+                state->selection_index = 0;
+                break;
+
+            default:
+                break;
+            }
+
+            state->pg--;
+            display(state, event.subsecond);
+
+            // Play beep
+            if (movement_button_should_sound())
+                watch_buzzer_play_note(BUZZER_NOTE_C8, 50);
+
+            state->alarm_held = false;
+        }
+        break;
+
+    case EVENT_ALARM_LONG_PRESS:
+        // Switch between locales
+        if (state->measurement_i == VOL)
+        {
+            if (state->pg == from)
+            {
+                state->from_is_us = !state->from_is_us;
+            }
+            else if (state->pg == to)
+            {
+                state->to_is_us = !state->to_is_us;
+            }
+
+            if (state->pg == from || state->pg == to)
+            {
+                display(state, event.subsecond);
+
+                // Play bleep
+                if (movement_button_should_sound())
+                    watch_buzzer_play_note(BUZZER_NOTE_E7, 50);
+            }
+        }
+
+        // Sets flag to increment input digit when alarm button held
+        if (state->pg == input)
+            state->alarm_held = true;
+
+        break;
+
+    case EVENT_ALARM_LONG_UP:
+        state->alarm_held = false;
+        break;
+
+    case EVENT_TIMEOUT:
+        movement_move_to_face(0);
+        break;
+
+    default:
+        return movement_default_loop_handler(event);
+    }
+
+    return true;
+}
+
+void kitchen_conversions_face_resign(void *context)
+{
+    (void)context;
+
+    // handle any cleanup before your watch face goes off-screen.
+}
diff --git a/watch-faces/complication/kitchen_conversions_face.h b/watch-faces/complication/kitchen_conversions_face.h
new file mode 100644 (file)
index 0000000..6f4251c
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 PrimmR
+ *
+ * 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 KITCHEN_CONVERSIONS_FACE_H_
+#define KITCHEN_CONVERSIONS_FACE_H_
+
+#include "movement.h"
+
+/*
+ * Kitchen Conversions
+ * A face that allows the user to convert between common kitchen units of measurement
+ *
+ * How to use
+ * ----------
+ * Short press the light button to move forward through menus, and long press to move backwards
+ *
+ * Press the alarm button to cycle through options in the menus
+ *
+ * When inputting a number, the light button moves forward one place and the alarm button increments the value of the selected digit
+ *
+ * To convert between Imperial (GB) and US (A) measurements of volume, hold the alarm button
+ *
+ */
+
+#define SCREEN_NUM 5
+
+// Names of each page
+typedef enum
+{
+    measurement,
+    from,
+    to,
+    input,
+    result,
+} page_t;
+
+#define DISPLAY_DIGITS 6
+
+// Settings when app is running
+typedef struct
+{
+    page_t pg;
+    uint8_t measurement_i;
+    uint8_t from_i;
+    bool from_is_us;
+    uint8_t to_i;
+    bool to_is_us;
+    uint32_t selection_value;
+    uint8_t selection_index;
+    bool alarm_held;
+} kitchen_conversions_state_t;
+
+void kitchen_conversions_face_setup(uint8_t watch_face_index, void **context_ptr);
+void kitchen_conversions_face_activate(void *context);
+bool kitchen_conversions_face_loop(movement_event_t event, void *context);
+void kitchen_conversions_face_resign(void *context);
+
+#define kitchen_conversions_face ((const watch_face_t){ \
+    kitchen_conversions_face_setup,                     \
+    kitchen_conversions_face_activate,                  \
+    kitchen_conversions_face_loop,                      \
+    kitchen_conversions_face_resign,                    \
+    NULL,                                               \
+})
+
+#endif // KITCHEN_CONVERSIONS_FACE_H_