--- /dev/null
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Konrad Rieck
+ *
+ * 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 "lis2dw_monitor_face.h"
+#include "watch.h"
+#include "watch_utility.h"
+
+/* Display frequency */
+#define DISPLAY_FREQUENCY 8
+
+/* Settings */
+#define NUM_SETTINGS 7
+
+static void _settings_title_display(lis2dw_monitor_state_t *state, char *buf1, char *buf2)
+{
+ char buf[10];
+ watch_display_text_with_fallback(WATCH_POSITION_TOP, buf1, buf2);
+ if (watch_get_lcd_type() != WATCH_LCD_TYPE_CUSTOM) {
+ snprintf(buf, sizeof(buf), "%2d", state->settings_page + 1);
+ watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf);
+ }
+}
+
+static bool _settings_blink(uint8_t subsecond)
+{
+ if (subsecond % 2 == 0) {
+ watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " ", " ");
+ return true;
+ }
+ return false;
+}
+
+static void _settings_mode_display(void *context, uint8_t subsecond)
+{
+ char buf[10];
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ _settings_title_display(state, "MODE ", "MO");
+ if (_settings_blink(subsecond))
+ return;
+
+ switch (state->ds.mode) {
+ case LIS2DW_MODE_LOW_POWER:
+ snprintf(buf, sizeof(buf), " LO ");
+ break;
+ case LIS2DW_MODE_HIGH_PERFORMANCE:
+ snprintf(buf, sizeof(buf), " HI ");
+ break;
+ case LIS2DW_MODE_ON_DEMAND:
+ snprintf(buf, sizeof(buf), " OD ");
+ break;
+ }
+ watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
+}
+
+static void _settings_mode_advance(void *context)
+{
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ switch (state->ds.mode) {
+ case LIS2DW_MODE_LOW_POWER:
+ state->ds.mode = LIS2DW_MODE_HIGH_PERFORMANCE;
+ break;
+ case LIS2DW_MODE_HIGH_PERFORMANCE:
+ state->ds.mode = LIS2DW_MODE_ON_DEMAND;
+ break;
+ case LIS2DW_MODE_ON_DEMAND:
+ state->ds.mode = LIS2DW_MODE_LOW_POWER;
+ break;
+ }
+}
+
+static void _settings_data_rate_display(void *context, uint8_t subsecond)
+{
+ char buf[10];
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ _settings_title_display(state, "RATE ", "DR");
+ if (_settings_blink(subsecond))
+ return;
+
+ switch (state->ds.data_rate) {
+ case LIS2DW_DATA_RATE_POWERDOWN:
+ snprintf(buf, sizeof(buf), " -- ");
+ break;
+ case LIS2DW_DATA_RATE_LOWEST:
+ snprintf(buf, sizeof(buf), " LO ");
+ break;
+ case LIS2DW_DATA_RATE_12_5_HZ:
+ snprintf(buf, sizeof(buf), " 12Hz");
+ break;
+ case LIS2DW_DATA_RATE_25_HZ:
+ snprintf(buf, sizeof(buf), " 25Hz");
+ break;
+ default:
+ snprintf(buf, sizeof(buf), " HI ");
+ break;
+ }
+ watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
+}
+
+static void _settings_data_rate_advance(void *context)
+{
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ switch (state->ds.data_rate) {
+ case LIS2DW_DATA_RATE_POWERDOWN:
+ state->ds.data_rate = LIS2DW_DATA_RATE_LOWEST;
+ break;
+ case LIS2DW_DATA_RATE_LOWEST:
+ state->ds.data_rate = LIS2DW_DATA_RATE_12_5_HZ;
+ break;
+ case LIS2DW_DATA_RATE_12_5_HZ:
+ state->ds.data_rate = LIS2DW_DATA_RATE_25_HZ;
+ break;
+ case LIS2DW_DATA_RATE_25_HZ:
+ state->ds.data_rate = LIS2DW_DATA_RATE_POWERDOWN;
+ break;
+ default:
+ state->ds.data_rate = LIS2DW_DATA_RATE_POWERDOWN;
+ break;
+ }
+}
+
+static void _settings_low_power_display(void *context, uint8_t subsecond)
+{
+ char buf[10];
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ _settings_title_display(state, "LO PM", "LP");
+ if (_settings_blink(subsecond))
+ return;
+
+ switch (state->ds.low_power) {
+ case LIS2DW_LP_MODE_1:
+ snprintf(buf, sizeof(buf), " L1 12");
+ break;
+ case LIS2DW_LP_MODE_2:
+ snprintf(buf, sizeof(buf), " L2 14");
+ break;
+ case LIS2DW_LP_MODE_3:
+ snprintf(buf, sizeof(buf), " L3 14");
+ break;
+ case LIS2DW_LP_MODE_4:
+ snprintf(buf, sizeof(buf), " L4 14");
+ break;
+ }
+ watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
+}
+
+static void _settings_low_power_advance(void *context)
+{
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ switch (state->ds.low_power) {
+ case LIS2DW_LP_MODE_1:
+ state->ds.low_power = LIS2DW_LP_MODE_2;
+ break;
+ case LIS2DW_LP_MODE_2:
+ state->ds.low_power = LIS2DW_LP_MODE_3;
+ break;
+ case LIS2DW_LP_MODE_3:
+ state->ds.low_power = LIS2DW_LP_MODE_4;
+ break;
+ case LIS2DW_LP_MODE_4:
+ state->ds.low_power = LIS2DW_LP_MODE_1;
+ break;
+ }
+}
+
+static void _settings_bwf_mode_display(void *context, uint8_t subsecond)
+{
+ char buf[10];
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ _settings_title_display(state, "BWF ", "BW");
+ if (_settings_blink(subsecond))
+ return;
+
+ switch (state->ds.bwf_mode) {
+ case LIS2DW_BANDWIDTH_FILTER_DIV2:
+ snprintf(buf, sizeof(buf), " 2 ");
+ break;
+ case LIS2DW_BANDWIDTH_FILTER_DIV4:
+ snprintf(buf, sizeof(buf), " 4 ");
+ break;
+ case LIS2DW_BANDWIDTH_FILTER_DIV10:
+ snprintf(buf, sizeof(buf), " 10 ");
+ break;
+ case LIS2DW_BANDWIDTH_FILTER_DIV20:
+ snprintf(buf, sizeof(buf), " 20 ");
+ break;
+ }
+ watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
+}
+
+static void _settings_bwf_mode_advance(void *context)
+{
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ switch (state->ds.bwf_mode) {
+ case LIS2DW_BANDWIDTH_FILTER_DIV2:
+ state->ds.bwf_mode = LIS2DW_BANDWIDTH_FILTER_DIV4;
+ break;
+ case LIS2DW_BANDWIDTH_FILTER_DIV4:
+ state->ds.bwf_mode = LIS2DW_BANDWIDTH_FILTER_DIV10;
+ break;
+ case LIS2DW_BANDWIDTH_FILTER_DIV10:
+ state->ds.bwf_mode = LIS2DW_BANDWIDTH_FILTER_DIV20;
+ break;
+ case LIS2DW_BANDWIDTH_FILTER_DIV20:
+ state->ds.bwf_mode = LIS2DW_BANDWIDTH_FILTER_DIV2;
+ break;
+ }
+}
+
+static void _settings_range_display(void *context, uint8_t subsecond)
+{
+ char buf[10];
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ _settings_title_display(state, "RANGE", "RA");
+ if (_settings_blink(subsecond))
+ return;
+
+ switch (state->ds.range) {
+ case LIS2DW_RANGE_2_G:
+ snprintf(buf, sizeof(buf), " 2g ");
+ break;
+ case LIS2DW_RANGE_4_G:
+ snprintf(buf, sizeof(buf), " 4g ");
+ break;
+ case LIS2DW_RANGE_8_G:
+ snprintf(buf, sizeof(buf), " 8g ");
+ break;
+ case LIS2DW_RANGE_16_G:
+ snprintf(buf, sizeof(buf), " 16g ");
+ break;
+ }
+ watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
+}
+
+static void _settings_range_advance(void *context)
+{
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ switch (state->ds.range) {
+ case LIS2DW_RANGE_2_G:
+ state->ds.range = LIS2DW_RANGE_4_G;
+ break;
+ case LIS2DW_RANGE_4_G:
+ state->ds.range = LIS2DW_RANGE_8_G;
+ break;
+ case LIS2DW_RANGE_8_G:
+ state->ds.range = LIS2DW_RANGE_16_G;
+ break;
+ case LIS2DW_RANGE_16_G:
+ state->ds.range = LIS2DW_RANGE_2_G;
+ break;
+ }
+}
+
+static void _settings_filter_display(void *context, uint8_t subsecond)
+{
+ char buf[10];
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ _settings_title_display(state, "FLT ", "FL");
+ if (_settings_blink(subsecond))
+ return;
+
+ switch (state->ds.filter) {
+ case LIS2DW_FILTER_LOW_PASS:
+ snprintf(buf, sizeof(buf), " LP ");
+ break;
+ case LIS2DW_FILTER_HIGH_PASS:
+ snprintf(buf, sizeof(buf), " HP ");
+ break;
+ }
+ watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
+}
+
+static void _settings_filter_advance(void *context)
+{
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ switch (state->ds.filter) {
+ case LIS2DW_FILTER_LOW_PASS:
+ state->ds.filter = LIS2DW_FILTER_HIGH_PASS;
+ break;
+ case LIS2DW_FILTER_HIGH_PASS:
+ state->ds.filter = LIS2DW_FILTER_LOW_PASS;
+ break;
+ }
+}
+
+static void _settings_low_noise_display(void *context, uint8_t subsecond)
+{
+ char buf[10];
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ _settings_title_display(state, "LO NO", "LN");
+ if (_settings_blink(subsecond))
+ return;
+
+ snprintf(buf, sizeof(buf), " %3s ", state->ds.low_noise ? "ON" : "OFF");
+ watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
+}
+
+static void _settings_low_noise_advance(void *context)
+{
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+ state->ds.low_noise = !state->ds.low_noise;
+}
+
+/* Play beep sound */
+static inline void _beep()
+{
+ if (!movement_button_should_sound())
+ return;
+ watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
+}
+
+/* Print lis2dw status to console. */
+static void _lis2dw_print_state(lis2dw_device_state_t *ds)
+{
+ printf("LIS2DW status:\n");
+ printf(" Power mode:\t%x\n", ds->mode);
+ printf(" Data rate:\t%x\n", ds->data_rate);
+ printf(" LP mode:\t%x\n", ds->low_power);
+ printf(" BW filter:\t%x\n", ds->bwf_mode);
+ printf(" Range:\t%x \n", ds->range);
+ printf(" Filter type:\t%x\n", ds->filter);
+ printf(" Low noise:\t%x\n", ds->low_noise);
+ printf("\n");
+}
+
+static void _lis2dw_get_state(lis2dw_device_state_t *ds)
+{
+ ds->mode = lis2dw_get_mode();
+ ds->data_rate = lis2dw_get_data_rate();
+ ds->low_power = lis2dw_get_low_power_mode();
+ ds->bwf_mode = lis2dw_get_bandwidth_filtering();
+ ds->range = lis2dw_get_range();
+ ds->filter = lis2dw_get_filter_type();
+ ds->low_noise = lis2dw_get_low_noise_mode();
+}
+
+static void _lis2dw_set_state(lis2dw_device_state_t *ds)
+{
+ lis2dw_set_mode(ds->mode);
+ lis2dw_set_data_rate(ds->data_rate);
+ lis2dw_set_low_power_mode(ds->low_power);
+ lis2dw_set_bandwidth_filtering(ds->bwf_mode);
+ lis2dw_set_range(ds->range);
+ lis2dw_set_filter_type(ds->filter);
+ lis2dw_set_low_noise_mode(ds->low_noise);
+
+ /* Additionally, set the background rate to the data rate. */
+ movement_set_accelerometer_background_rate(ds->data_rate);
+}
+
+static void _monitor_display(lis2dw_monitor_state_t *state)
+{
+ char buf[10];
+
+ snprintf(buf, sizeof(buf), " %C ", "XYZ"[state->axis]);
+ watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, buf, buf);
+
+ snprintf(buf, sizeof(buf), "%2d", state->axis + 1);
+ watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf);
+
+ if (state->show_title) {
+ snprintf(buf, sizeof(buf), "LIS2DW");
+ watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
+ return;
+ }
+
+ if (state->ds.data_rate == LIS2DW_DATA_RATE_POWERDOWN) {
+ /* No measurements available. */
+ snprintf(buf, sizeof(buf), " -- ");
+ } else if (state->axis == 0) {
+ char sign = (state->reading.x) >= 0 ? ' ' : '-';
+ snprintf(buf, sizeof(buf), "%c%.5d", sign, abs(state->reading.x));
+ } else if (state->axis == 1) {
+ char sign = (state->reading.y) >= 0 ? ' ' : '-';
+ snprintf(buf, sizeof(buf), "%c%.5d", sign, abs(state->reading.y));
+ } else {
+ char sign = (state->reading.z) >= 0 ? ' ' : '-';
+ snprintf(buf, sizeof(buf), "%c%.5d", sign, abs(state->reading.z));
+ }
+ watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
+}
+
+static void _monitor_update(lis2dw_monitor_state_t *state)
+{
+ lis2dw_fifo_t fifo;
+ float x = 0, y = 0, z = 0;
+
+ lis2dw_read_fifo(&fifo);
+ if (fifo.count == 0) {
+ return;
+ }
+
+ /* Add up samples in fifo */
+ for (uint8_t i = 0; i < fifo.count; i++) {
+ x += fifo.readings[i].x;
+ y += fifo.readings[i].y;
+ z += fifo.readings[i].z;
+ }
+
+ /* Divide by number of samples */
+ state->reading.x = (int16_t) (x / fifo.count);
+ state->reading.y = (int16_t) (y / fifo.count);
+ state->reading.z = (int16_t) (z / fifo.count);
+
+ lis2dw_clear_fifo();
+}
+
+static void _switch_to_monitor(lis2dw_monitor_state_t *state)
+{
+ /* Switch to recording page */
+ movement_request_tick_frequency(DISPLAY_FREQUENCY);
+ state->page = PAGE_LIS2DW_MONITOR;
+ state->show_title = DISPLAY_FREQUENCY;
+ _monitor_display(state);
+}
+
+static void _switch_to_settings(lis2dw_monitor_state_t *state)
+{
+ /* Switch to chirping page */
+ movement_request_tick_frequency(4);
+ state->page = PAGE_LIS2DW_SETTINGS;
+ state->settings_page = 0;
+ state->settings[state->settings_page].display(state, 0);
+}
+
+static bool _monitor_loop(movement_event_t event, void *context)
+{
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ switch (event.event_type) {
+ case EVENT_ACTIVATE:
+ watch_clear_colon();
+ _monitor_update(state);
+ _monitor_display(state);
+ break;
+ case EVENT_TICK:
+ _monitor_update(state);
+ _monitor_display(state);
+ state->show_title = (state->show_title > 0) ? state->show_title - 1 : 0;
+ break;
+ case EVENT_ALARM_BUTTON_UP:
+ state->axis = (state->axis + 1) % 3;
+ _monitor_display(state);
+ break;
+ case EVENT_LIGHT_BUTTON_DOWN:
+ /* Do nothing. */
+ break;
+ case EVENT_LIGHT_LONG_PRESS:
+ _switch_to_settings(state);
+ _beep();
+ break;
+ default:
+ movement_default_loop_handler(event);
+ break;
+ }
+
+ return true;
+}
+
+static bool _settings_loop(movement_event_t event, void *context)
+{
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ switch (event.event_type) {
+ case EVENT_ACTIVATE:
+ case EVENT_TICK:
+ state->settings[state->settings_page].display(context, event.subsecond);
+ break;
+ case EVENT_LIGHT_BUTTON_UP:
+ state->settings_page = (state->settings_page + 1) % NUM_SETTINGS;
+ state->settings[state->settings_page].display(context, event.subsecond);
+ break;
+ case EVENT_MODE_BUTTON_UP:
+ _lis2dw_set_state(&state->ds);
+ _lis2dw_print_state(&state->ds);
+ _switch_to_monitor(state);
+ _beep();
+ break;
+ case EVENT_LIGHT_BUTTON_DOWN:
+ /* Do nothing. */
+ break;
+ case EVENT_ALARM_BUTTON_UP:
+ /* Advance current settings */
+ state->settings[state->settings_page].advance(context);
+ state->settings[state->settings_page].display(context, event.subsecond);
+ break;
+ default:
+ _lis2dw_set_state(&state->ds);
+ movement_default_loop_handler(event);
+ break;
+ }
+ return true;
+}
+
+void lis2dw_monitor_face_setup(uint8_t watch_face_index, void **context_ptr)
+{
+ (void) watch_face_index;
+ if (*context_ptr == NULL) {
+ *context_ptr = malloc(sizeof(lis2dw_monitor_state_t));
+ memset(*context_ptr, 0, sizeof(lis2dw_monitor_state_t));
+ }
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) * context_ptr;
+
+ /* Default setup */
+ state->axis = 0;
+
+ /* Initialize settings */
+ uint8_t settings_page = 0;
+ state->settings = malloc(NUM_SETTINGS * sizeof(lis2dw_settings_t));
+ state->settings[settings_page].display = _settings_mode_display;
+ state->settings[settings_page].advance = _settings_mode_advance;
+ settings_page++;
+ state->settings[settings_page].display = _settings_data_rate_display;
+ state->settings[settings_page].advance = _settings_data_rate_advance;
+ settings_page++;
+ state->settings[settings_page].display = _settings_low_power_display;
+ state->settings[settings_page].advance = _settings_low_power_advance;
+ settings_page++;
+ state->settings[settings_page].display = _settings_bwf_mode_display;
+ state->settings[settings_page].advance = _settings_bwf_mode_advance;
+ settings_page++;
+ state->settings[settings_page].display = _settings_range_display;
+ state->settings[settings_page].advance = _settings_range_advance;
+ settings_page++;
+ state->settings[settings_page].display = _settings_filter_display;
+ state->settings[settings_page].advance = _settings_filter_advance;
+ settings_page++;
+ state->settings[settings_page].display = _settings_low_noise_display;
+ state->settings[settings_page].advance = _settings_low_noise_advance;
+ settings_page++;
+}
+
+void lis2dw_monitor_face_activate(void *context)
+{
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ /* Setup lis2dw to run in background at 12.5 Hz sampling rate. */
+ movement_set_accelerometer_background_rate(LIS2DW_DATA_RATE_12_5_HZ);
+
+ /* Enable fifo and clear it. */
+ lis2dw_enable_fifo();
+ lis2dw_clear_fifo();
+
+ /* Print lis2dw status to console. */
+ _lis2dw_get_state(&state->ds);
+ _lis2dw_print_state(&state->ds);
+
+ /* Switch to monitor page. */
+ _switch_to_monitor(state);
+}
+
+bool lis2dw_monitor_face_loop(movement_event_t event, void *context)
+{
+ lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context;
+
+ switch (state->page) {
+ default:
+ case PAGE_LIS2DW_MONITOR:
+ return _monitor_loop(event, context);
+ case PAGE_LIS2DW_SETTINGS:
+ return _settings_loop(event, context);
+ }
+}
+
+void lis2dw_monitor_face_resign(void *context)
+{
+ (void) context;
+ lis2dw_clear_fifo();
+ lis2dw_disable_fifo();
+}
+
+movement_watch_face_advisory_t lis2dw_monitor_face_advise(void *context)
+{
+ (void) context;
+ movement_watch_face_advisory_t retval = { 0 };
+ return retval;
+}