summaryrefslogtreecommitdiffstats
path: root/movement
diff options
context:
space:
mode:
Diffstat (limited to 'movement')
-rw-r--r--movement/README.md297
-rwxr-xr-xmovement/make/.gitignore1
-rwxr-xr-xmovement/make/Makefile39
-rw-r--r--movement/movement.c244
-rw-r--r--movement/movement.h190
-rw-r--r--movement/movement_config.h21
-rw-r--r--movement/watch_faces/clock/simple_clock_face.c85
-rw-r--r--movement/watch_faces/clock/simple_clock_face.h21
-rw-r--r--movement/watch_faces/complications/beats_face.c75
-rw-r--r--movement/watch_faces/complications/beats_face.h20
-rw-r--r--movement/watch_faces/complications/pulsometer_face.c90
-rw-r--r--movement/watch_faces/complications/pulsometer_face.h25
-rw-r--r--movement/watch_faces/demos/character_set_face.c51
-rw-r--r--movement/watch_faces/demos/character_set_face.h19
-rw-r--r--movement/watch_faces/settings/preferences_face.c168
-rw-r--r--movement/watch_faces/settings/preferences_face.h19
-rw-r--r--movement/watch_faces/settings/set_time_face.c112
-rw-r--r--movement/watch_faces/settings/set_time_face.h19
-rw-r--r--movement/watch_faces/thermistor/thermistor_driver.c76
-rw-r--r--movement/watch_faces/thermistor/thermistor_driver.h13
-rw-r--r--movement/watch_faces/thermistor/thermistor_logging_face.c114
-rw-r--r--movement/watch_faces/thermistor/thermistor_logging_face.h35
-rw-r--r--movement/watch_faces/thermistor/thermistor_readout_face.c73
-rw-r--r--movement/watch_faces/thermistor/thermistor_readout_face.h19
24 files changed, 1826 insertions, 0 deletions
diff --git a/movement/README.md b/movement/README.md
new file mode 100644
index 00000000..009234f8
--- /dev/null
+++ b/movement/README.md
@@ -0,0 +1,297 @@
+Movement: the community watch face app
+======================================
+
+The Sensor Watch Library allows you to write your own bare-metal applications for the Sensor Watch. This is great if you want full control over the code running on the device, but it also means that you may have to implement your own UI for many common tasks like setting the time or illuminating the screen.
+
+**Movement** is an application that manages the display of different screens of content on the watch. These screens are called **watch faces**. Watch faces can be passive displays of information like a clock or a calendar, or they can be fully interactive user interfaces like the Preferences face, which allows the user to customize Movement's behavior. Movement handles the instantiation of your watch face and manages transitions between screens. It also provides a low-power sleep mode, triggered after a period of inactivity, to preserve the watch battery.
+
+Several faces are provided that offer baseline functionality like a clock, a settings screen and an interface for setting the time. You can change and reorder the watch faces that Movement displays by editing `movement_config.h`, and you can write your own watch face using the guidance in this document.
+
+Watch Face API
+--------------
+
+You can implement a watch face using just four functions:
+
+* `watch_face_setup`
+* `watch_face_activate`
+* `watch_face_loop`
+* `watch_face_resign`
+
+A fifth optional function, `watch_face_wants_background_task`, has not yet had its implementation ironed out, but it will be added to the guide at a later date.
+
+To create a new watch face, you should create a new C header and source file in the watch-faces folder (i.e. for a watch face that displays moon phases: `moon_phase_face.h`, `moon_phase_face.c`), and implement these functions with your own unique prefix (i.e. `moon_phase_face_setup`). Then declare your watch face in your header file as follows:
+
+```c
+static const watch_face_t moon_phase_face = {
+ moon_phase_face_setup,
+ moon_phase_face_activate,
+ moon_phase_face_loop,
+ moon_phase_face_resign,
+ NULL // or moon_phase_face_wants_background_task, if you implemented this function
+};
+```
+
+This section will go over how each function works. The section headings use the watch_face prefix, but know that you should implement each function with your own prefix as described above.
+
+### watch_face_setup
+
+If you have worked with Arduino, this function is similar to setup() in that it is called at first boot. In our case, it is also called when waking from sleep mode. You will be passed three parameters:
+
+* `settings` - a pointer to the global Movement settings. You can use this to inform how you present your display to the user (i.e. taking into account whether they have silenced the buttons, or if they prefer 12 or 24-hour mode). You can also change these settings if you like.
+* `position` - The 0-indexed position of your watch face in the list of faces.
+* `context_ptr` - A pointer to a pointer. On first run, the pointee will be NULL. If you need to keep track of any state within your watch face, you should check if it is NULL, and if so, set its value to a pointer to some value or struct that will keep track of that state. For example, the Preferences face needs to keep track of which page the user is viewing (just an integer), whereas the Pulsometer face needs to track several different properties in a struct.
+
+Beyond setting up the context pointer, you may want to configure any peripherals that your watch face requires; for example, a temperature watch face that reads a thermistor output may want to configure the ADC here. Still, to save power, you should avoid leaving the peripheral enabled, and wait to set pin function in the activate function.
+
+It was mentioned above but it's worth mentioning again: this function will be called again after waking from sleep mode, since sleep mode disables all of the device's pins and peripherals. This would give the temperature watch face a chance to re-configure the ADC.
+
+### watch_face_activate
+
+This function is called just before your watch enters the foreground. If your watch face has any segments or text that is always displayed, you may want to set that here. In addition, if your watch face depends on data from a peripheral (like that temperature watch face), you will likely want to enable that peripheral and set any required pin modes here. This function is also passed a pointer to the settings and your application context.
+
+### watch_face_loop
+
+This is a lot like your loop() function in Arduinoland in that it is called repeatedly whenever your watch face is on screen. There is one crucial difference though: it is called less often. By default, this function is called once per second, and in response to events like button presses. You can request a more frequent tick interval by calling `movement_request_tick_frequency` with any power of 2 from 1 to 128.
+
+In addition to the settings and context, this function receives another parameter: an `event`. This is a struct containing information about the event that triggered the update. You mostly need to check the `event_type` to determine what kind of event triggered the loop. A detailed list of all events is provided at the bottom of this document.
+
+There is also a `subsecond` property on the event that contains the fractional second of the event. If you are using 1 Hz updates, subsecond will always be 0.
+
+You should set up a switch statement that handles, at the very least, the `EVENT_TICK` and `EVENT_MODE_BUTTON_UP` event types. The mode button up event occurs when the user presses the MODE button. **Your loop function SHOULD call the movement_move_to_next_face function in response to this event.** If you have a good reason to override this behavior (e.g. your user interface requires all three buttons), your watch face MUST call the movement_move_to_next_face function in response to the EVENT_MODE_LONG_PRESS event. If you fail to do this, the user will become stuck on your watch face.
+
+### watch_face_resign
+
+This function is called just before your watch face goes off screen. You should disable any peripherals you enabled in `watch_face_activate`. If you requested a tick frequency other than 1 Hz at any point in your code, **you must reset it to 1 Hz when you resign**. The watch_face_resign function is passed the same settings and context as the other functions.
+
+Putting it into practice: the Pulsometer watch face
+---------------------------------------------------
+
+Let's take a look at a watch face to see how these pieces fit together. A *pulsometer* is [a mechanical watch complication designed to determine someone's pulse](https://www.ablogtowatch.com/longines-pulsometer-chronograph-watch/) by counting their heartbeats: you start the pulsometer, count heartbeats, and stop it when you reach the specified number. The needle will point to the pulse rate.
+
+Let's implement a pulsometer for the Sensor Watch. These files are in the repository as `pulsometer_face.h` and `pulsometer_face.c`, but we'll walk through them inline here.
+
+### pulsometer_face.h
+
+First, let's take a look at the header file. First we include the Movement header file, which defines the various types we need to build a watch face:
+
+```c
+#include "movement.h"
+```
+
+The pulsometer needs to track certain state to do its job, so we define a struct to contain our watch face's context:
+
+```c
+typedef struct {
+ bool measuring;
+ int16_t pulse;
+ int16_t ticks;
+} pulsometer_state_t;
+```
+
+Finally, we define the four required functions, and define the watch face struct that users will use to add the face to their watch:
+
+```c
+void pulsometer_face_setup(movement_settings_t *settings, void ** context_ptr);
+void pulsometer_face_activate(movement_settings_t *settings, void *context);
+bool pulsometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
+void pulsometer_face_resign(movement_settings_t *settings, void *context);
+
+static const watch_face_t pulsometer_face = {
+ pulsometer_face_setup,
+ pulsometer_face_activate,
+ pulsometer_face_loop,
+ pulsometer_face_resign,
+ NULL
+};
+```
+
+### pulsometer_face.c
+
+Now let's look at the implementation of the Pulsometer face. First up, we have a couple of definitions that we'll reference in the code:
+
+```c
+#define PULSOMETER_FACE_FREQUENCY_FACTOR (4ul) // refresh rate will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.)
+#define PULSOMETER_FACE_FREQUENCY (1 << PULSOMETER_FACE_FREQUENCY_FACTOR)
+```
+
+These define the tick frequency: when the pulsometer widget is updating the screen, it will request 16 Hz updates (2^4).
+
+#### Watch Face Setup
+
+```c
+void pulsometer_face_setup(movement_settings_t *settings, void ** context_ptr) {
+ (void) settings;
+ if (*context_ptr == NULL) *context_ptr = malloc(sizeof(pulsometer_state_t));
+}
+```
+
+The `(void) settings;` line just silences a compiler warning about the unused parameter. The next line checks if the context pointer is NULL, and if so, allocates a `pulsometer_state_t`-sized chunk of memory to hold our state.
+
+#### Watch Face Activation
+
+```c
+void pulsometer_face_activate(movement_settings_t *settings, void *context) {
+ (void) settings;
+ memset(context, 0, sizeof(pulsometer_state_t));
+}
+```
+
+The pulsometer face doesn't need to keep track of context in between appearances; there's no need to keep displaying an old pulse reading hours or days after it was taken. So this line just sets the context to all zeroes before the watch face goes on screen.
+
+#### Watch Face Loop
+
+Next we have the loop function. First things first: it fetches our application context, and casts it to a `pulsometer_state_t` type so we can make use of it. It also creates a buffer for any text we plan to put on screen, and declares a switch statement for handling events:
+
+```c
+bool pulsometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
+ (void) settings;
+ pulsometer_state_t *pulsometer_state = (pulsometer_state_t *)context;
+ char buf[14];
+ switch (event.event_type) {
+```
+
+Let's go through each case one by one. In response to the user releasing the MODE button, we tell Movement to move to the next watch face.
+
+```c
+case EVENT_MODE_BUTTON_UP:
+ movement_move_to_next_face();
+ break;
+```
+
+Similarly in response to the user pressing the LIGHT button, we tell Movement to illuminate the LED. Movement does not do this automatically, in case your watch face UI has another use for the LIGHT button.
+
+```c
+case EVENT_LIGHT_BUTTON_DOWN:
+ movement_illuminate_led();
+ break;
+```
+
+The ALARM button is the main button the user will use to interact with the pulsometer. In response to the user pressing the ALARM button, we begin a measurement. We also request a faster tick frequency, so that we can update the display at 16 Hz.
+
+```c
+case EVENT_ALARM_BUTTON_DOWN:
+ pulsometer_state->measuring = true;
+ pulsometer_state->pulse = 0xFFFF;
+ pulsometer_state->ticks = 0;
+ movement_request_tick_frequency(PULSOMETER_FACE_FREQUENCY);
+ break;
+```
+
+When the user releases the ALARM button, we finish the measurement. We also scale the update frequency back down to 1 Hz.
+
+```c
+case EVENT_ALARM_BUTTON_UP:
+case EVENT_ALARM_LONG_PRESS:
+ pulsometer_state->measuring = false;
+ movement_request_tick_frequency(1);
+ break;
+```
+
+The tick event handler is long, but handles all display updates. The first half of this conditional handles the case where we haven't yet measured anything: it just loops through five screens with instructions, and increments the tick count.
+
+```c
+case EVENT_TICK:
+ if (pulsometer_state->pulse == 0 && !pulsometer_state->measuring) {
+ switch (pulsometer_state->ticks % 5) {
+ case 0:
+ watch_display_string(" Hold ", 2);
+ break;
+ case 1:
+ watch_display_string(" Alarn", 4);
+ break;
+ case 2:
+ watch_display_string("+ Count ", 0);
+ break;
+ case 3:
+ watch_display_string(" 30Beats ", 0);
+ break;
+ case 4:
+ watch_clear_display();
+ break;
+ }
+ pulsometer_state->ticks = (pulsometer_state->ticks + 1) % 5;
+```
+
+The second half of the conditional handles the case where we are measuring or have a measurement to display. It does the math, updates the screen, and increments the tick count if needed.
+
+```c
+ } else {
+ if (pulsometer_state->measuring && pulsometer_state->ticks) {
+ pulsometer_state->pulse = (int16_t)((30.0 * ((float)(60 << PULSOMETER_FACE_FREQUENCY_FACTOR) / (float)pulsometer_state->ticks)) + 0.5);
+ }
+ if (pulsometer_state->pulse > 240) {
+ watch_display_string(" Hi", 0);
+ } else if (pulsometer_state->pulse < 40) {
+ watch_display_string(" Lo", 0);
+ } else {
+ sprintf(buf, " %-3dbpn", pulsometer_state->pulse);
+ watch_display_string(buf, 0);
+ }
+ if (pulsometer_state->measuring) pulsometer_state->ticks++;
+ }
+ break;
+```
+
+Finally, the timeout event. After a period of inactivity (configurable from one to thirty minutes), Movement will send this event to indicate that the user has not interacted with your watch face in some time. Watch faces do not need to resign when they receive the timeout event, but depending on what kind of information your watch face displays, you may want to resign by asking Movement to return to the first watch face (usually a clock). The pulsometer widget has no need to remain on screen, so it opts to return to the clock when it receives the timeout event.
+
+```c
+case EVENT_TIMEOUT:
+ movement_move_to_face(0);
+ break;
+```
+
+#### Watch Face Resignation
+
+The resign function doesn't have to do much here; it just resets the tick frequency to 1 Hz.
+
+```c
+void pulsometer_face_resign(movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+ movement_request_tick_frequency(1);
+}
+```
+
+And that's that!
+
+Low Energy Mode
+---------------
+
+To save energy, the watch enters a low energy mode after a timeout period (confugurable from 1 hour to 7 days). In this mode, the watch will turn off all pins and peripherals except for the screen and real-time clock, and will wake up once a minute to allow the current watch face to update its display.
+
+Movement Event Types
+--------------------
+
+### EVENT_ACTIVATE
+
+You will receive this event when your watch face is entering the foreground. You can treat it like a tick event and just update the display.
+
+### EVENT_TICK
+
+This is the most common event type. Your watch face is being called as a result of the real-time clock ticking. By default this tick occurs once per second, but you can request more frequent updates.
+
+### EVENT_LIGHT_BUTTON_DOWN, EVENT_MODE_BUTTON_DOWN, EVENT_ALARM_BUTTON_DOWN
+
+Your watch face receives these events when one of the buttons is initially depressed, but before it is released.
+
+### EVENT_LIGHT_BUTTON_UP, EVENT_MODE_BUTTON_UP, EVENT_ALARM_BUTTON_UP
+
+Your watch face receives these events when one of these buttons is released quickly after being depressed (i.e. held for less than one second).
+
+### EVENT_LIGHT_LONG_PRESS, EVENT_MODE_LONG_PRESS, EVENT_ALARM_LONG_PRESS
+
+Your watch face receives these events when one of these buttons is released after having been held down for more than two seconds.
+
+### EVENT_TIMEOUT
+
+Your watch face receives this event after it has has been inactive for a while. You may want to resign here, depending on your watch face's intended use case.
+
+### EVENT_LOW_ENERGY_UPDATE
+
+If your watch face is in the foreground when the watch goes into low energy mode, you will receive an `EVENT_LOW_ENERGY_UPDATE` event once a minute (at the top of the minute) so that you can update the screen. Note however that when you receive this event, all pins and peripherals other than the RTC will have been disabled to save energy. If your display is clock or calendar oriented, this is fine. But if your display requires polling an I2C sensor or reading a value with the ADC, you won't be able to do this. You should either display the name of the watch face in response to the low power tick, or ensure that you resign before low power mode triggers (you can do this by calling `movement_move_to_face(0)` in your `EVENT_TIMEOUT` handler).
+
+**Your watch face MUST NOT wake up peripherals in response to a low energy update event.** The purpose of this mode is to consume as little energy as possible during the (potentially long) intervals when it's unlikely the user is wearing or looking at the watch.
+
+### EVENT_BACKGROUND_TASK
+
+The `EVENT_BACKGROUND_TASK` event is not yet implemented, but the plan is for this event type to allow waking peripherals even in low power mode. More information will be added in a future version of this guide.
diff --git a/movement/make/.gitignore b/movement/make/.gitignore
new file mode 100755
index 00000000..3722ac63
--- /dev/null
+++ b/movement/make/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/movement/make/Makefile b/movement/make/Makefile
new file mode 100755
index 00000000..a937222b
--- /dev/null
+++ b/movement/make/Makefile
@@ -0,0 +1,39 @@
+# Leave this line at the top of the file; it has all the watch library sources and includes.
+TOP = ../..
+include $(TOP)/make.mk
+
+# If you add any other subdirectories with header files you wish to include, add them after ../
+# Note that you will need to add a backslash at the end of any line you wish to continue, i.e.
+# INCLUDES += \
+# -I../ \
+# -I../drivers/ \
+# -I../watch_faces/fitness/
+INCLUDES += \
+ -I../ \
+ -I../watch_faces/ \
+ -I../watch_faces/clock/ \
+ -I../watch_faces/settings/ \
+ -I../watch_faces/complications/ \
+ -I../watch_faces/thermistor/ \
+ -I../watch_faces/demos/ \
+
+# If you add any other source files you wish to compile, add them after ../app.c
+# Note that you will need to add a backslash at the end of any line you wish to continue, i.e.
+# SRCS += \
+# ../movement.c \
+# ../drivers/lis2dh.c \
+# ../watch_faces/fitness/step_count_face.c
+SRCS += \
+ ../movement.c \
+ ../watch_faces/clock/simple_clock_face.c \
+ ../watch_faces/settings/preferences_face.c \
+ ../watch_faces/settings/set_time_face.c \
+ ../watch_faces/complications/pulsometer_face.c \
+ ../watch_faces/thermistor/thermistor_driver.c \
+ ../watch_faces/thermistor/thermistor_readout_face.c \
+ ../watch_faces/thermistor/thermistor_logging_face.c \
+ ../watch_faces/demos/character_set_face.c \
+ ../watch_faces/complications/beats_face.c \
+
+# Leave this line at the bottom of the file; it has all the targets for making your project.
+include $(TOP)/rules.mk
diff --git a/movement/movement.c b/movement/movement.c
new file mode 100644
index 00000000..5622f17f
--- /dev/null
+++ b/movement/movement.c
@@ -0,0 +1,244 @@
+#include <stdio.h>
+#include <string.h>
+#include <limits.h>
+#include "watch.h"
+#include "movement.h"
+#include "movement_config.h"
+
+movement_state_t movement_state;
+void * watch_face_contexts[MOVEMENT_NUM_FACES];
+const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 3600, 7200, 21600, 43200, 86400, 172800, 604800};
+const int32_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800};
+movement_event_t event;
+
+void cb_mode_btn_interrupt();
+void cb_light_btn_interrupt();
+void cb_alarm_btn_interrupt();
+void cb_alarm_btn_extwake();
+void cb_alarm_fired();
+void cb_tick();
+
+static inline void _movement_reset_inactivity_countdown() {
+ movement_state.le_mode_ticks = movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval];
+ movement_state.timeout_ticks = movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval];
+}
+
+void _movement_handle_background_tasks() {
+ for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
+ // For each face, if the watch face wants a background task...
+ if (watch_faces[i].wants_background_task != NULL && watch_faces[i].wants_background_task(&movement_state.settings, watch_face_contexts[i])) {
+ // ...we give it one. pretty straightforward!
+ movement_event_t background_event = { EVENT_BACKGROUND_TASK, 0 };
+ watch_faces[i].loop(background_event, &movement_state.settings, watch_face_contexts[i]);
+ }
+ }
+ movement_state.needs_background_tasks_handled = false;
+}
+
+void movement_request_tick_frequency(uint8_t freq) {
+ watch_rtc_disable_all_periodic_callbacks();
+ movement_state.subsecond = 0;
+ movement_state.tick_frequency = freq;
+ watch_rtc_register_periodic_callback(cb_tick, freq);
+}
+
+void movement_illuminate_led() {
+ watch_set_led_color(movement_state.settings.bit.led_red_color ? (0xF | movement_state.settings.bit.led_red_color << 4) : 0,
+ movement_state.settings.bit.led_green_color ? (0xF | movement_state.settings.bit.led_green_color << 4) : 0);
+ movement_state.led_on = true;
+ movement_state.light_ticks = movement_state.settings.bit.led_duration * 2;
+}
+
+void movement_move_to_face(uint8_t watch_face_index) {
+ movement_state.watch_face_changed = true;
+ movement_state.next_watch_face = watch_face_index;
+}
+
+void movement_move_to_next_face() {
+ movement_move_to_face((movement_state.current_watch_face + 1) % MOVEMENT_NUM_FACES);
+}
+
+void app_init() {
+ memset(&movement_state, 0, sizeof(movement_state));
+
+ movement_state.settings.bit.led_green_color = 0xF;
+ movement_state.settings.bit.button_should_sound = true;
+ movement_state.settings.bit.le_interval = 1;
+ movement_state.settings.bit.led_duration = 1;
+ _movement_reset_inactivity_countdown();
+}
+
+void app_wake_from_backup() {
+ // This app does not support BACKUP mode.
+}
+
+void app_setup() {
+ static bool is_first_launch = true;
+
+ if (is_first_launch) {
+ for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
+ watch_face_contexts[i] = NULL;
+ is_first_launch = false;
+ }
+
+ // set up the 1 minute alarm (for background tasks and low power updates)
+ watch_date_time alarm_time;
+ alarm_time.reg = 0;
+ alarm_time.unit.second = 59; // after a match, the alarm fires at the next rising edge of CLK_RTC_CNT, so 59 seconds lets us update at :00
+ watch_rtc_register_alarm_callback(cb_alarm_fired, alarm_time, ALARM_MATCH_SS);
+ }
+ if (movement_state.le_mode_ticks != -1) {
+ watch_disable_extwake_interrupt(BTN_ALARM);
+
+ watch_enable_external_interrupts();
+ watch_register_interrupt_callback(BTN_MODE, cb_mode_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
+ watch_register_interrupt_callback(BTN_LIGHT, cb_light_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
+ watch_register_interrupt_callback(BTN_ALARM, cb_alarm_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
+
+ watch_enable_buzzer();
+ watch_enable_leds();
+ watch_enable_display();
+
+ movement_request_tick_frequency(1);
+
+ for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
+ watch_faces[i].setup(&movement_state.settings, &watch_face_contexts[i]);
+ }
+
+ watch_faces[0].activate(&movement_state.settings, watch_face_contexts[0]);
+ event.subsecond = 0;
+ event.event_type = EVENT_ACTIVATE;
+ }
+}
+
+void app_prepare_for_standby() {
+}
+
+void app_wake_from_standby() {
+}
+
+bool app_loop() {
+ if (movement_state.watch_face_changed) {
+ if (movement_state.settings.bit.button_should_sound) {
+ // low note for nonzero case, high note for return to watch_face 0
+ watch_buzzer_play_note(movement_state.next_watch_face ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50);
+ }
+ watch_faces[movement_state.current_watch_face].resign(&movement_state.settings, watch_face_contexts[movement_state.current_watch_face]);
+ movement_state.current_watch_face = movement_state.next_watch_face;
+ watch_clear_display();
+ watch_faces[movement_state.current_watch_face].activate(&movement_state.settings, watch_face_contexts[movement_state.current_watch_face]);
+ event.subsecond = 0;
+ event.event_type = EVENT_ACTIVATE;
+ movement_state.watch_face_changed = false;
+ }
+
+ // if the LED is on and should be off, turn it off
+ if (movement_state.led_on && movement_state.light_ticks == 0) {
+ // unless the user is holding down the LIGHT button, in which case, give them more time.
+ if (watch_get_pin_level(BTN_LIGHT)) {
+ movement_state.light_ticks = 3;
+ } else {
+ watch_set_led_off();
+ movement_state.led_on = false;
+ }
+ }
+
+ // if we have timed out of our timeout countdown, give the app a hint that they can resign.
+ if (movement_state.timeout_ticks == 0) {
+ event.event_type = EVENT_TIMEOUT;
+ }
+
+ // handle background tasks, if the alarm handler told us we need to
+ if (movement_state.needs_background_tasks_handled) _movement_handle_background_tasks();
+
+ // if we have timed out of our low energy mode countdown, enter low energy mode.
+ if (movement_state.le_mode_ticks == 0) {
+ movement_state.le_mode_ticks = -1;
+ watch_register_extwake_callback(BTN_ALARM, cb_alarm_btn_extwake, true);
+ event.event_type = EVENT_NONE;
+ event.subsecond = 0;
+
+ // this is a little mini-runloop.
+ // as long as le_mode_ticks is -1 (i.e. we are in low energy mode), we wake up here, update the screen, and go right back to sleep.
+ while (movement_state.le_mode_ticks == -1) {
+ // we also have to handle background tasks here in the mini-runloop
+ if (movement_state.needs_background_tasks_handled) _movement_handle_background_tasks();
+
+ event.event_type = EVENT_LOW_ENERGY_UPDATE;
+ watch_faces[movement_state.current_watch_face].loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_watch_face]);
+ watch_enter_sleep_mode();
+ }
+ // as soon as le_mode_ticks is reset by the extwake handler, we bail out of the loop and reactivate ourselves.
+ event.event_type = EVENT_ACTIVATE;
+ // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks.
+ // need to figure out if there's a better heuristic for determining how we woke up.
+ app_setup();
+ }
+
+ static bool can_sleep = true;
+
+ if (event.event_type) {
+ event.subsecond = movement_state.subsecond;
+ can_sleep = watch_faces[movement_state.current_watch_face].loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_watch_face]);
+ event.event_type = EVENT_NONE;
+ event.subsecond = 0;
+ }
+
+ return can_sleep && !movement_state.led_on;
+}
+
+movement_event_type_t _figure_out_button_event(movement_event_type_t button_down_event_type, uint8_t *down_timestamp) {
+ watch_date_time date_time = watch_rtc_get_date_time();
+ if (*down_timestamp) {
+ uint8_t diff = ((61 + date_time.unit.second) - *down_timestamp) % 60;
+ *down_timestamp = 0;
+ if (diff > 1) return button_down_event_type + 2;
+ else return button_down_event_type + 1;
+ } else {
+ *down_timestamp = date_time.unit.second + 1;
+ return button_down_event_type;
+ }
+}
+
+void cb_light_btn_interrupt() {
+ _movement_reset_inactivity_countdown();
+ event.event_type = _figure_out_button_event(EVENT_LIGHT_BUTTON_DOWN, &movement_state.light_down_timestamp);
+}
+
+void cb_mode_btn_interrupt() {
+ _movement_reset_inactivity_countdown();
+ event.event_type = _figure_out_button_event(EVENT_MODE_BUTTON_DOWN, &movement_state.mode_down_timestamp);
+}
+
+void cb_alarm_btn_interrupt() {
+ _movement_reset_inactivity_countdown();
+ event.event_type = _figure_out_button_event(EVENT_ALARM_BUTTON_DOWN, &movement_state.alarm_down_timestamp);
+}
+
+void cb_alarm_btn_extwake() {
+ // wake up!
+ _movement_reset_inactivity_countdown();
+}
+
+void cb_alarm_fired() {
+ movement_state.needs_background_tasks_handled = true;
+}
+
+void cb_tick() {
+ event.event_type = EVENT_TICK;
+ watch_date_time date_time = watch_rtc_get_date_time();
+ if (date_time.unit.second != movement_state.last_second) {
+ // TODO: since we time the LED with the 1 Hz tick, the actual time lit can vary depending on whether the
+ // user hit it just before or just after a tick. If we time this with the system tick we can do better.
+ if (movement_state.light_ticks) movement_state.light_ticks--;
+
+ // TODO: can we consolidate these two ticks?
+ if (movement_state.settings.bit.le_interval && movement_state.le_mode_ticks > 0) movement_state.le_mode_ticks--;
+ if (movement_state.timeout_ticks > 0) movement_state.timeout_ticks--;
+
+ movement_state.last_second = date_time.unit.second;
+ movement_state.subsecond = 0;
+ } else {
+ movement_state.subsecond++;
+ }
+}
diff --git a/movement/movement.h b/movement/movement.h
new file mode 100644
index 00000000..cb162b95
--- /dev/null
+++ b/movement/movement.h
@@ -0,0 +1,190 @@
+#ifndef MOVEMENT_H_
+#define MOVEMENT_H_
+#include <stdio.h>
+#include <stdbool.h>
+
+// TODO: none of this is implemented
+typedef union {
+ struct {
+ uint32_t reserved : 14;
+ uint32_t button_should_sound : 1; // if true, pressing a button emits a sound.
+ uint32_t to_interval : 2; // an inactivity interval for asking the active face to resign.
+ uint32_t le_interval : 3; // 0 to disable low energy mode, or an inactivity interval for going into low energy mode.
+ uint32_t led_duration : 2; // how many seconds to shine the LED for (x2), or 0 to disable it.
+ uint32_t led_red_color : 4; // for general purpose illumination, the red LED value (0-15)
+ uint32_t led_green_color : 4; // for general purpose illumination, the green LED value (0-15)
+
+ // while Movement itself doesn't implement a clock or display units, it may make sense to include some
+ // global settings for watch faces to check. The 12/24 hour preference could inform a clock or a
+ // time-oriented complication like a sunrise/sunset timer, and a simple locale preference could tell an
+ // altimeter to display feet or meters as easily as it tells a thermometer to display degrees in F or C.
+ uint32_t clock_mode_24h : 1; // indicates whether clock should use 12 or 24 hour mode.
+ uint32_t use_imperial_units : 1; // indicates whether to use metric units (the default) or imperial.
+ } bit;
+ uint32_t value;
+} movement_settings_t;
+
+typedef enum {
+ EVENT_NONE = 0, // There is no event to report.
+ EVENT_ACTIVATE, // Your watch face is entering the foreground.
+ EVENT_TICK, // Most common event type. Your watch face is being called from the tick callback.
+ EVENT_LOW_ENERGY_UPDATE, // If the watch is in low energy mode and you are in the foreground, you will get a chance to update the display once per minute.
+ EVENT_BACKGROUND_TASK, // Your watch face is being invoked to perform a background task. Don't update the display here; you may not be in the foreground.
+ EVENT_TIMEOUT, // Your watch face has been inactive for a while. You may want to resign, depending on your watch face's intended use case.
+ EVENT_LIGHT_BUTTON_DOWN, // The light button has been pressed, but not yet released.
+ EVENT_LIGHT_BUTTON_UP, // The light button was pressed and released.
+ EVENT_LIGHT_LONG_PRESS, // The light button was held for >2 seconds, and released.
+ EVENT_MODE_BUTTON_DOWN, // The mode button has been pressed, but not yet released.
+ EVENT_MODE_BUTTON_UP, // The mode button was pressed and released.
+ EVENT_MODE_LONG_PRESS, // The mode button was held for >2 seconds, and released.
+ EVENT_ALARM_BUTTON_DOWN, // The alarm button has been pressed, but not yet released.
+ EVENT_ALARM_BUTTON_UP, // The alarm button was pressed and released.
+ EVENT_ALARM_LONG_PRESS, // The alarm button was held for >2 seconds, and released.
+} movement_event_type_t;
+
+typedef struct {
+ uint8_t event_type;
+ uint8_t subsecond;
+} movement_event_t;
+
+/** @brief Perform setup for your watch face.
+ * @details It's tempting to say this is 'one-time' setup, but technically this function is called more than
+ * once. When the watch first boots, this function is called with a NULL context_ptr, indicating
+ * that it is the first run. At this time you should set context_ptr to something non-NULL if you
+ * need to keep track of any state in your watch face. If your watch face requires any other setup,
+ * like configuring a pin mode or a peripheral, you may want to do that here too.
+ * This function will be called again after waking from sleep mode, since sleep mode disables all
+ * of the device's pins and peripherals.
+ * @param settings A pointer to the global Movement settings. You can use this to inform how you present your
+ * display to the user (i.e. taking into account whether they have silenced the buttons, or if
+ * they prefer 12 or 24-hour mode). You can also change these settings if you like.
+ * @param context_ptr A pointer to a pointer; at first invocation, this value will be NULL, and you can set it
+ * to any value you like. Subsequent invocations will pass in whatever value you previously
+ * set. You may want to check if this is NULL and if so, allocate some space to store any
+ * data required for your watch face.
+ *
+ */
+typedef void (*watch_face_setup)(movement_settings_t *settings, void ** context_ptr);
+
+/** @brief Prepare to go on-screen.
+ * @details This function is called just before your watch enters the foreground. If your watch face has any
+ * segments or text that is always displayed, you may want to set that here. In addition, if your
+ * watch face depends on data from a peripheral (like an I2C sensor), you will likely want to enable
+ * that peripheral here. In addition, if your watch face requires an update frequncy other than 1 Hz,
+ * you may want to request that here using the movement_request_tick_frequency function.
+ * @param settings A pointer to the global Movement settings. @see watch_face_setup.
+ * @param context A pointer to your watch face's context. @see watch_face_setup.
+ *
+ */
+typedef void (*watch_face_activate)(movement_settings_t *settings, void *context);
+
+/** @brief Handle events and update the display.
+ * @details This function is called in response to an event. You should set up a switch statement that handles,
+ * at the very least, the EVENT_TICK and EVENT_MODE_BUTTON_UP event types. The tick event happens once
+ * per second (or more frequently if you asked for a faster tick with movement_request_tick_frequency).
+ * The mode button up event occurs when the user presses the MODE button. **Your loop function SHOULD
+ * call the movement_move_to_next_face function in response to this event.** If you have a good reason
+ * to override this behavior (e.g. your user interface requires all three buttons), your watch face MUST
+ * call the movement_move_to_next_face function in response to the EVENT_MODE_LONG_PRESS event. If you
+ * fail to do this, the user will become stuck on your watch face.
+ * @param event A struct containing information about the event, including its type. @see movement_event_type_t
+ * for a list of all possible event types.
+ * @param settings A pointer to the global Movement settings. @see watch_face_setup.
+ * @param context A pointer to your application's context. @see watch_face_setup.
+ * @return true if Movement can enter STANDBY mode; false to keep it awake. You should almost always return true.
+ * @note There are two event types that require some extra thought:
+ The EVENT_LOW_ENERGY_UPDATE event type is a special case. If you are in the foreground when the watch
+ goes into low energy mode, you will receive this tick once a minute (at the top of the minute) so that
+ you can update the screen. Great! But! When you receive this event, all pins and peripherals other than
+ the RTC will have been disabled to save energy. If your display is clock or calendar oriented, this is
+ fine. But if your display requires polling an I2C sensor or reading a value with the ADC, you won't be
+ able to do this. You should either display the name of the watch face in response to the low power tick,
+ or ensure that you resign before low power mode triggers, (e.g. by calling movement_move_to_face(0)).
+ **Your watch face MUST NOT wake up peripherals in response to a low power tick.** The purpose of this
+ mode is to consume as little energy as possible during the (potentially long) intervals when it's
+ unlikely the user is wearing or looking at the watch.
+ EVENT_BACKGROUND_TASK is also a special case. @see watch_face_wants_background_task for details.
+ */
+typedef bool (*watch_face_loop)(movement_event_t event, movement_settings_t *settings, void *context);
+
+/** @brief Prepare to go off-screen.
+ * @details This function is called before your watch face enters the background. If you requested a tick
+ * frequency other than the standard 1 Hz, **you must call movement_request_tick_frequency(1) here**
+ * to reset to 1 Hz. You should also disable any peripherals you enabled when you entered the foreground.
+ * @param settings A pointer to the global Movement settings. @see watch_face_setup.
+ * @param context A pointer to your application's context. @see watch_face_setup.
+ */
+typedef void (*watch_face_resign)(movement_settings_t *settings, void *context);
+
+/** @brief OPTIONAL. Request an opportunity to run a background task.
+ * @warning NOT YET IMPLEMENTED.
+ * @details Most apps will not need this function, but if you provide it, Movement will call it once per minute in
+ * both active and low power modes, regardless of whether your app is in the foreground. You can check the
+ * current time to determine whether you require a background task. If you return true here, Movement will
+ * immediately call your loop function with an EVENT_BACKGROUND_TASK event. Note that it will not call your
+ * activate or deactivate functions, since you are not going on screen.
+ *
+ * Examples of background tasks:
+ * - Wake and play a sound when an alarm or timer has been triggered.
+ * - Check the state of an RTC interrupt pin or the timestamp of an RTC interrupt event.
+ * - Log a data point from a sensor, and then return to sleep mode.
+ *
+ * Guidelines for background tasks:
+ * - Assume all peripherals and pins other than the RTC will be disabled when you get an EVENT_BACKGROUND_TASK.
+ * - Even if your background task involves only the RTC peripheral, try to request background tasks sparingly.
+ * - If your background task involves an external pin or peripheral, request background tasks no more than once per hour.
+ * - If you need to enable a pin or a peripheral to perform your task, return it to its original state afterwards.
+ *
+ * @param settings A pointer to the global Movement settings. @see watch_face_setup.
+ * @param context A pointer to your application's context. @see watch_face_setup.
+ * @return true to request a background task; false otherwise.
+ */
+typedef bool (*watch_face_wants_background_task)(movement_settings_t *settings, void *context);
+
+typedef struct {
+ watch_face_setup setup;
+ watch_face_activate activate;
+ watch_face_loop loop;
+ watch_face_resign resign;
+ watch_face_wants_background_task wants_background_task;
+} watch_face_t;
+
+typedef struct {
+ // properties stored in BACKUP register
+ movement_settings_t settings;
+
+ // transient properties
+ int16_t current_watch_face;
+ int16_t next_watch_face;
+ bool watch_face_changed;
+
+ // LED stuff
+ uint8_t light_ticks;
+ bool led_on;
+
+ // button tracking for long press
+ uint8_t light_down_timestamp;
+ uint8_t mode_down_timestamp;
+ uint8_t alarm_down_timestamp;
+
+ // background task handling
+ bool needs_background_tasks_handled;
+
+ // low energy mode countdown
+ int32_t le_mode_ticks;
+
+ // app resignation countdown (TODO: consolidate with LE countdown?)
+ int16_t timeout_ticks;
+
+ // stuff for subsecond tracking
+ uint8_t tick_frequency;
+ uint8_t last_second;
+ uint8_t subsecond;
+} movement_state_t;
+
+void movement_move_to_face(uint8_t watch_face_index);
+void movement_move_to_next_face();
+void movement_illuminate_led();
+void movement_request_tick_frequency(uint8_t freq);
+
+#endif // MOVEMENT_H_
diff --git a/movement/movement_config.h b/movement/movement_config.h
new file mode 100644
index 00000000..3e911477
--- /dev/null
+++ b/movement/movement_config.h
@@ -0,0 +1,21 @@
+#ifndef MOVEMENT_CONFIG_H_
+#define MOVEMENT_CONFIG_H_
+
+#include "simple_clock_face.h"
+#include "preferences_face.h"
+#include "set_time_face.h"
+#include "pulsometer_face.h"
+#include "thermistor_readout_face.h"
+#include "thermistor_logging_face.h"
+#include "character_set_face.h"
+#include "beats_face.h"
+
+const watch_face_t watch_faces[] = {
+ simple_clock_face,
+ preferences_face,
+ set_time_face,
+};
+
+#define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t))
+
+#endif // MOVEMENT_CONFIG_H_
diff --git a/movement/watch_faces/clock/simple_clock_face.c b/movement/watch_faces/clock/simple_clock_face.c
new file mode 100644
index 00000000..9e279d95
--- /dev/null
+++ b/movement/watch_faces/clock/simple_clock_face.c
@@ -0,0 +1,85 @@
+#include <stdlib.h>
+#include "simple_clock_face.h"
+#include "watch.h"
+#include "watch_utility.h"
+
+void simple_clock_face_setup(movement_settings_t *settings, void ** context_ptr) {
+ (void) settings;
+ // the only context we need is the timestamp of the previous tick.
+ if (*context_ptr == NULL) *context_ptr = malloc(sizeof(uint32_t));
+}
+
+void simple_clock_face_activate(movement_settings_t *settings, void *context) {
+ if (watch_tick_animation_is_running()) watch_stop_tick_animation();
+
+ if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
+
+ watch_set_colon();
+ // this ensures that none of the timestamp fields will match, so we can re-render them all.
+ *((uint32_t *)context) = 0xFFFFFFFF;
+}
+
+bool simple_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
+ char buf[11];
+ uint8_t pos;
+
+ watch_date_time date_time;
+ uint32_t previous_date_time;
+ switch (event.event_type) {
+ case EVENT_ACTIVATE:
+ case EVENT_TICK:
+ case EVENT_TIMEOUT:
+ case EVENT_LOW_ENERGY_UPDATE:
+ date_time = watch_rtc_get_date_time();
+ previous_date_time = *((uint32_t *)context);
+ *((uint32_t *)context) = date_time.reg;
+
+ if (date_time.reg >> 6 == previous_date_time >> 6 && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
+ // everything before seconds is the same, don't waste cycles setting those segments.
+ pos = 8;
+ sprintf(buf, "%02d", date_time.unit.second);
+ } else if (date_time.reg >> 12 == previous_date_time >> 12 && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
+ // everything before minutes is the same.
+ pos = 6;
+ sprintf(buf, "%02d%02d", date_time.unit.minute, date_time.unit.second);
+ } else {
+ // other stuff changed; let's do it all.
+ if (!settings->bit.clock_mode_24h) {
+ // if we are in 12 hour mode, do some cleanup.
+ if (date_time.unit.hour < 12) {
+ watch_clear_indicator(WATCH_INDICATOR_PM);
+ } else {
+ watch_set_indicator(WATCH_INDICATOR_PM);
+ }
+ date_time.unit.hour %= 12;
+ if (date_time.unit.hour == 0) date_time.unit.hour = 12;
+ }
+ pos = 0;
+ if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
+ if (!watch_tick_animation_is_running()) watch_start_tick_animation(500);
+ sprintf(buf, "%s%2d%2d%02d ", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute);
+ } else {
+ sprintf(buf, "%s%2d%2d%02d%02d", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
+ }
+ }
+ watch_display_string(buf, pos);
+ break;
+ case EVENT_MODE_BUTTON_UP:
+ movement_move_to_next_face();
+ return false;
+ case EVENT_LIGHT_BUTTON_DOWN:
+ movement_illuminate_led();
+ break;
+ case EVENT_ALARM_BUTTON_UP:
+ break;
+ default:
+ break;
+ }
+
+ return true;
+}
+
+void simple_clock_face_resign(movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+}
diff --git a/movement/watch_faces/clock/simple_clock_face.h b/movement/watch_faces/clock/simple_clock_face.h
new file mode 100644
index 00000000..3db894d2
--- /dev/null
+++ b/movement/watch_faces/clock/simple_clock_face.h
@@ -0,0 +1,21 @@
+#ifndef SIMPLE_CLOCK_FACE_H_
+#define SIMPLE_CLOCK_FACE_H_
+
+#include "movement.h"
+
+void simple_clock_face_setup(movement_settings_t *settings, void ** context_ptr);
+void simple_clock_face_activate(movement_settings_t *settings, void *context);
+bool simple_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
+void simple_clock_face_resign(movement_settings_t *settings, void *context);
+
+uint8_t simple_clock_face_get_weekday(uint16_t day, uint16_t month, uint16_t year);
+
+static const watch_face_t simple_clock_face = {
+ simple_clock_face_setup,
+ simple_clock_face_activate,
+ simple_clock_face_loop,
+ simple_clock_face_resign,
+ NULL
+};
+
+#endif // SIMPLE_CLOCK_FACE_H_ \ No newline at end of file
diff --git a/movement/watch_faces/complications/beats_face.c b/movement/watch_faces/complications/beats_face.c
new file mode 100644
index 00000000..73a82719
--- /dev/null
+++ b/movement/watch_faces/complications/beats_face.c
@@ -0,0 +1,75 @@
+#include <stdlib.h>
+#include <string.h>
+#include "beats_face.h"
+#include "watch.h"
+
+const uint8_t UTC_OFFSET = 4; // set to your current UTC offset to see correct beats time
+const uint8_t BEAT_REFRESH_FREQUENCY = 8;
+
+void beats_face_setup(movement_settings_t *settings, void ** context_ptr) {
+ (void) settings;
+ (void) context_ptr;
+}
+
+void beats_face_activate(movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+ movement_request_tick_frequency(BEAT_REFRESH_FREQUENCY);
+}
+
+bool beats_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+
+ char buf[14];
+ float beats;
+
+ watch_date_time date_time;
+ switch (event.event_type) {
+ case EVENT_TICK:
+ date_time = watch_rtc_get_date_time();
+ beats = clock2beats(date_time.unit.hour, date_time.unit.minute, date_time.unit.second, event.subsecond, UTC_OFFSET);
+ sprintf(buf, "bt %6.0f", beats * 100);
+
+ watch_display_string(buf, 0);
+ break;
+ case EVENT_LOW_ENERGY_UPDATE:
+ date_time = watch_rtc_get_date_time();
+ beats = clock2beats(date_time.unit.hour, date_time.unit.minute, date_time.unit.second, event.subsecond, UTC_OFFSET);
+ sprintf(buf, "bt %4d ", (int)beats);
+
+ watch_display_string(buf, 0);
+ break;
+ case EVENT_MODE_BUTTON_UP:
+ movement_move_to_next_face();
+ break;
+ case EVENT_LIGHT_BUTTON_DOWN:
+ movement_illuminate_led();
+ break;
+ case EVENT_ALARM_BUTTON_DOWN:
+ case EVENT_ALARM_BUTTON_UP:
+ case EVENT_ALARM_LONG_PRESS:
+ default:
+ break;
+ }
+
+ return true;
+}
+
+void beats_face_resign(movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+ movement_request_tick_frequency(1);
+}
+
+float clock2beats(uint16_t hours, uint16_t minutes, uint16_t seconds, uint16_t subseconds, int16_t utc_offset) {
+ float beats = seconds + ((float)subseconds / (float)BEAT_REFRESH_FREQUENCY);
+ beats += 60 * minutes;
+ beats += (float)hours * 60 * 60;
+ beats += (utc_offset + 1) * 60 * 60; // offset from utc + 1 since beats in in UTC+1
+
+ beats /= 86.4; // convert to beats
+ while(beats > 1000) beats -= 1000; // beats %= 1000 but for a float
+
+ return beats;
+} \ No newline at end of file
diff --git a/movement/watch_faces/complications/beats_face.h b/movement/watch_faces/complications/beats_face.h
new file mode 100644
index 00000000..fe34f5aa
--- /dev/null
+++ b/movement/watch_faces/complications/beats_face.h
@@ -0,0 +1,20 @@
+#ifndef BEATS_FACE_H_
+#define BEATS_FACE_H_
+
+#include "movement.h"
+
+float clock2beats(uint16_t, uint16_t, uint16_t, uint16_t, int16_t);
+void beats_face_setup(movement_settings_t *settings, void ** context_ptr);
+void beats_face_activate(movement_settings_t *settings, void *context);
+bool beats_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
+void beats_face_resign(movement_settings_t *settings, void *context);
+
+static const watch_face_t beats_face = {
+ beats_face_setup,
+ beats_face_activate,
+ beats_face_loop,
+ beats_face_resign,
+ NULL
+};
+
+#endif // BEATS_FACE_H_ \ No newline at end of file
diff --git a/movement/watch_faces/complications/pulsometer_face.c b/movement/watch_faces/complications/pulsometer_face.c
new file mode 100644
index 00000000..d54d8ddf
--- /dev/null
+++ b/movement/watch_faces/complications/pulsometer_face.c
@@ -0,0 +1,90 @@
+#include <stdlib.h>
+#include <string.h>
+#include "pulsometer_face.h"
+#include "watch.h"
+
+#define PULSOMETER_FACE_FREQUENCY_FACTOR (4ul) // refresh rate will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.)
+#define PULSOMETER_FACE_FREQUENCY (1 << PULSOMETER_FACE_FREQUENCY_FACTOR)
+
+void pulsometer_face_setup(movement_settings_t *settings, void ** context_ptr) {
+ (void) settings;
+ if (*context_ptr == NULL) *context_ptr = malloc(sizeof(pulsometer_state_t));
+}
+
+void pulsometer_face_activate(movement_settings_t *settings, void *context) {
+ (void) settings;
+ memset(context, 0, sizeof(pulsometer_state_t));
+}
+
+bool pulsometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
+ (void) settings;
+ pulsometer_state_t *pulsometer_state = (pulsometer_state_t *)context;
+ char buf[14];
+ switch (event.event_type) {
+ case EVENT_MODE_BUTTON_UP:
+ movement_move_to_next_face();
+ break;
+ case EVENT_LIGHT_BUTTON_DOWN:
+ movement_illuminate_led();
+ break;
+ case EVENT_ALARM_BUTTON_DOWN:
+ pulsometer_state->measuring = true;
+ pulsometer_state->pulse = 0xFFFF;
+ pulsometer_state->ticks = 0;
+ movement_request_tick_frequency(PULSOMETER_FACE_FREQUENCY);
+ break;
+ case EVENT_ALARM_BUTTON_UP:
+ case EVENT_ALARM_LONG_PRESS:
+ pulsometer_state->measuring = false;
+ movement_request_tick_frequency(1);
+ break;
+ case EVENT_TICK:
+ if (pulsometer_state->pulse == 0 && !pulsometer_state->measuring) {
+ switch (pulsometer_state->ticks % 5) {
+ case 0:
+ watch_display_string(" Hold ", 2);
+ break;
+ case 1:
+ watch_display_string(" Alarn", 4);
+ break;
+ case 2:
+ watch_display_string("+ Count ", 0);
+ break;
+ case 3:
+ watch_display_string(" 30Beats ", 0);
+ break;
+ case 4:
+ watch_clear_display();
+ break;
+ }
+ pulsometer_state->ticks = (pulsometer_state->ticks + 1) % 5;
+ } else {
+ if (pulsometer_state->measuring && pulsometer_state->ticks) {
+ pulsometer_state->pulse = (int16_t)((30.0 * ((float)(60 << PULSOMETER_FACE_FREQUENCY_FACTOR) / (float)pulsometer_state->ticks)) + 0.5);
+ }
+ if (pulsometer_state->pulse > 240) {
+ watch_display_string(" Hi", 0);
+ } else if (pulsometer_state->pulse < 40) {
+ watch_display_string(" Lo", 0);
+ } else {
+ sprintf(buf, " %-3dbpn", pulsometer_state->pulse);
+ watch_display_string(buf, 0);
+ }
+ if (pulsometer_state->measuring) pulsometer_state->ticks++;
+ }
+ break;
+ case EVENT_TIMEOUT:
+ movement_move_to_face(0);
+ break;
+ default:
+ break;
+ }
+
+ return true;
+}
+
+void pulsometer_face_resign(movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+ movement_request_tick_frequency(1);
+}
diff --git a/movement/watch_faces/complications/pulsometer_face.h b/movement/watch_faces/complications/pulsometer_face.h
new file mode 100644
index 00000000..cdb5b977
--- /dev/null
+++ b/movement/watch_faces/complications/pulsometer_face.h
@@ -0,0 +1,25 @@
+#ifndef PULSOMETER_FACE_H_
+#define PULSOMETER_FACE_H_
+
+#include "movement.h"
+
+typedef struct {
+ bool measuring;
+ int16_t pulse;
+ int16_t ticks;
+} pulsometer_state_t;
+
+void pulsometer_face_setup(movement_settings_t *settings, void ** context_ptr);
+void pulsometer_face_activate(movement_settings_t *settings, void *context);
+bool pulsometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
+void pulsometer_face_resign(movement_settings_t *settings, void *context);
+
+static const watch_face_t pulsometer_face = {
+ pulsometer_face_setup,
+ pulsometer_face_activate,
+ pulsometer_face_loop,
+ pulsometer_face_resign,
+ NULL
+};
+
+#endif // PULSOMETER_FACE_H_ \ No newline at end of file
diff --git a/movement/watch_faces/demos/character_set_face.c b/movement/watch_faces/demos/character_set_face.c
new file mode 100644
index 00000000..7daea5a9
--- /dev/null
+++ b/movement/watch_faces/demos/character_set_face.c
@@ -0,0 +1,51 @@
+#include <stdlib.h>
+#include <string.h>
+#include "character_set_face.h"
+#include "watch.h"
+
+void character_set_face_setup(movement_settings_t *settings, void ** context_ptr) {
+ (void) settings;
+ if (*context_ptr == NULL) *context_ptr = malloc(sizeof(char));
+}
+
+void character_set_face_activate(movement_settings_t *settings, void *context) {
+ (void) settings;
+ char *c = (char *)context;
+ *c = '@';
+}
+
+bool character_set_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
+ (void) settings;
+ char *c = (char *)context;
+ char buf[11];
+ switch (event.event_type) {
+ case EVENT_MODE_BUTTON_UP:
+ movement_move_to_next_face();
+ break;
+ case EVENT_LIGHT_BUTTON_DOWN:
+ movement_illuminate_led();
+ break;
+ case EVENT_ALARM_BUTTON_UP:
+ *c = (*c) + 1;
+ if (*c & 0x80) *c = ' ';
+ // fall through
+ case EVENT_ACTIVATE:
+ sprintf(buf, "%c%c%c%c%c%c%c%c%c%c", *c, *c, *c, *c, *c, *c, *c, *c, *c, *c);
+ watch_display_string(buf, 0);
+ break;
+ case EVENT_TICK:
+ break;
+ case EVENT_TIMEOUT:
+ movement_move_to_face(0);
+ break;
+ default:
+ break;
+ }
+
+ return true;
+}
+
+void character_set_face_resign(movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+}
diff --git a/movement/watch_faces/demos/character_set_face.h b/movement/watch_faces/demos/character_set_face.h
new file mode 100644
index 00000000..b27a8359
--- /dev/null
+++ b/movement/watch_faces/demos/character_set_face.h
@@ -0,0 +1,19 @@
+#ifndef CHARACTER_SET_FACE_H_
+#define CHARACTER_SET_FACE_H_
+
+#include "movement.h"
+
+void character_set_face_setup(movement_settings_t *settings, void ** context_ptr);
+void character_set_face_activate(movement_settings_t *settings, void *context);
+bool character_set_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
+void character_set_face_resign(movement_settings_t *settings, void *context);
+
+static const watch_face_t character_set_face = {
+ character_set_face_setup,
+ character_set_face_activate,
+ character_set_face_loop,
+ character_set_face_resign,
+ NULL
+};
+
+#endif // CHARACTER_SET_FACE_H_ \ No newline at end of file
diff --git a/movement/watch_faces/settings/preferences_face.c b/movement/watch_faces/settings/preferences_face.c
new file mode 100644
index 00000000..afe71034
--- /dev/null
+++ b/movement/watch_faces/settings/preferences_face.c
@@ -0,0 +1,168 @@
+#include <stdlib.h>
+#include "preferences_face.h"
+#include "watch.h"
+
+#define PREFERENCES_FACE_NUM_PREFEFENCES (7)
+const char preferences_face_titles[PREFERENCES_FACE_NUM_PREFEFENCES][11] = {
+ "CL ", // Clock: 12 or 24 hour
+ "BT Beep ", // Buttons: should they beep?
+ "TO ", // Timeout: how long before we snap back to the clock face?
+ "LE ", // Low Energy mode: how long before it engages?
+ "LT ", // Light: duration
+ "LT grn ", // Light: green component
+ "LT red ", // Light: red component
+};
+
+void preferences_face_setup(movement_settings_t *settings, void ** context_ptr) {
+ (void) settings;
+ if (*context_ptr == NULL) *context_ptr = malloc(sizeof(uint8_t));
+}
+
+void preferences_face_activate(movement_settings_t *settings, void *context) {
+ (void) settings;
+ *((uint8_t *)context) = 0;
+ movement_request_tick_frequency(4); // we need to manually blink some pixels
+}
+
+bool preferences_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
+ uint8_t current_page = *((uint8_t *)context);
+ switch (event.event_type) {
+ case EVENT_MODE_BUTTON_UP:
+ watch_set_led_off();
+ movement_move_to_next_face();
+ return false;
+ case EVENT_LIGHT_BUTTON_UP:
+ current_page = (current_page + 1) % PREFERENCES_FACE_NUM_PREFEFENCES;
+ *((uint8_t *)context) = current_page;
+ break;
+ case EVENT_ALARM_BUTTON_UP:
+ switch (current_page) {
+ case 0:
+ settings->bit.clock_mode_24h = !(settings->bit.clock_mode_24h);
+ break;
+ case 1:
+ settings->bit.button_should_sound = !(settings->bit.button_should_sound);
+ break;
+ case 2:
+ settings->bit.to_interval = settings->bit.to_interval + 1;
+ break;
+ case 3:
+ settings->bit.le_interval = settings->bit.le_interval + 1;
+ break;
+ case 4:
+ settings->bit.led_duration = settings->bit.led_duration + 1;
+ break;
+ case 5:
+ settings->bit.led_green_color = settings->bit.led_green_color + 1;
+ break;
+ case 6:
+ settings->bit.led_red_color = settings->bit.led_red_color + 1;
+ break;
+ }
+ break;
+ case EVENT_TIMEOUT:
+ movement_move_to_face(0);
+ break;
+ default:
+ break;
+ }
+
+ watch_display_string((char *)preferences_face_titles[current_page], 0);
+
+ // blink active setting on even-numbered quarter-seconds
+ if (event.subsecond % 2) {
+ char buf[8];
+ switch (current_page) {
+ case 0:
+ if (settings->bit.clock_mode_24h) watch_display_string("24h", 4);
+ else watch_display_string("12h", 4);
+ break;
+ case 1:
+ if (settings->bit.button_should_sound) watch_display_string("y", 9);
+ else watch_display_string("n", 9);
+ break;
+ case 2:
+ switch (settings->bit.to_interval) {
+ case 0:
+ watch_display_string("60 sec", 4);
+ break;
+ case 1:
+ watch_display_string("2 n&in", 4);
+ break;
+ case 2:
+ watch_display_string("5 n&in", 4);
+ break;
+ case 3:
+ watch_display_string("30n&in", 4);
+ break;
+ }
+ break;
+ case 3:
+ switch (settings->bit.le_interval) {
+ case 0:
+ watch_display_string(" never", 4);
+ break;
+ case 1:
+ watch_display_string("1 hour", 4);
+ break;
+ case 2:
+ watch_display_string("2 hour", 4);
+ break;
+ case 3:
+ watch_display_string("6 hour", 4);
+ break;
+ case 4:
+ watch_display_string("12 hr", 4);
+ break;
+ case 5:
+ watch_display_string(" 1 day", 4);
+ break;
+ case 6:
+ watch_display_string(" 2 day", 4);
+ break;
+ case 7:
+ watch_display_string(" 7 day", 4);
+ break;
+ }
+ break;
+ case 4:
+ if (settings->bit.led_duration) {
+ // TODO: since we time the LED with the 1 Hz tick, the actual time lit can vary depending
+ // on whether the user hit it just before or just after a tick. so the setting is "1-2 s",
+ // "3-4 s", or "5-6 s". If we time this with the system tick we can do better.
+ // see also cb_tick at the bottom of movement.c
+ sprintf(buf, " %1d-%1d s", settings->bit.led_duration * 2 - 1, settings->bit.led_duration * 2);
+ watch_display_string(buf, 4);
+ } else {
+ watch_display_string("no LEd", 4);
+ }
+ break;
+ case 5:
+ sprintf(buf, "%2d", settings->bit.led_green_color);
+ watch_display_string(buf, 8);
+ break;
+ case 6:
+ sprintf(buf, "%2d", settings->bit.led_red_color);
+ watch_display_string(buf, 8);
+ break;
+ }
+ }
+
+ // on LED color select screns, preview the color.
+ if (current_page >= 5) {
+ watch_set_led_color(settings->bit.led_red_color ? (0xF | settings->bit.led_red_color << 4) : 0,
+ settings->bit.led_green_color ? (0xF | settings->bit.led_green_color << 4) : 0);
+ // return false so the watch stays awake (needed for the PWM driver to function).
+ return false;
+ }
+
+ watch_set_led_off();
+ return true;
+}
+
+void preferences_face_resign(movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+ watch_set_led_off();
+ movement_request_tick_frequency(1);
+}
diff --git a/movement/watch_faces/settings/preferences_face.h b/movement/watch_faces/settings/preferences_face.h
new file mode 100644
index 00000000..af628ba3
--- /dev/null
+++ b/movement/watch_faces/settings/preferences_face.h
@@ -0,0 +1,19 @@
+#ifndef PREFERENCES_FACE_H_
+#define PREFERENCES_FACE_H_
+
+#include "movement.h"
+
+void preferences_face_setup(movement_settings_t *settings, void ** context_ptr);
+void preferences_face_activate(movement_settings_t *settings, void *context);
+bool preferences_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
+void preferences_face_resign(movement_settings_t *settings, void *context);
+
+static const watch_face_t preferences_face = {
+ preferences_face_setup,
+ preferences_face_activate,
+ preferences_face_loop,
+ preferences_face_resign,
+ NULL
+};
+
+#endif // PREFERENCES_FACE_H_ \ No newline at end of file
diff --git a/movement/watch_faces/settings/set_time_face.c b/movement/watch_faces/settings/set_time_face.c
new file mode 100644
index 00000000..6b82c68b
--- /dev/null
+++ b/movement/watch_faces/settings/set_time_face.c
@@ -0,0 +1,112 @@
+#include <stdlib.h>
+#include "set_time_face.h"
+#include "watch.h"
+
+#define SET_TIME_FACE_NUM_SETTINGS (6)
+const char set_time_face_titles[SET_TIME_FACE_NUM_SETTINGS][3] = {"HR", "MN", "SE", "YR", "MO", "DA"};
+
+void set_time_face_setup(movement_settings_t *settings, void ** context_ptr) {
+ (void) settings;
+ if (*context_ptr == NULL) *context_ptr = malloc(sizeof(uint8_t));
+}
+
+void set_time_face_activate(movement_settings_t *settings, void *context) {
+ (void) settings;
+ *((uint8_t *)context) = 0;
+ movement_request_tick_frequency(4);
+}
+
+bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
+ uint8_t current_page = *((uint8_t *)context);
+ const uint8_t days_in_month[12] = {31, 28, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31};
+ watch_date_time date_time = watch_rtc_get_date_time();
+
+ switch (event.event_type) {
+ case EVENT_MODE_BUTTON_UP:
+ movement_move_to_next_face();
+ return false;
+ case EVENT_LIGHT_BUTTON_UP:
+ current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS;
+ *((uint8_t *)context) = current_page;
+ break;
+ case EVENT_ALARM_BUTTON_UP:
+ switch (current_page) {
+ case 0: // hour
+ date_time.unit.hour = (date_time.unit.hour + 1) % 24;
+ break;
+ case 1: // minute
+ date_time.unit.minute = (date_time.unit.minute + 1) % 60;
+ break;
+ case 2: // second
+ date_time.unit.second = 0;
+ break;
+ case 3: // year
+ // only allow 2021-2030. fix this sometime next decade
+ date_time.unit.year = ((date_time.unit.year % 10) + 1);
+ break;
+ case 4: // month
+ date_time.unit.month = (date_time.unit.month % 12) + 1;
+ break;
+ case 5: // day
+ date_time.unit.day = date_time.unit.day + 1;
+ // can't set to the 29th on a leap year. if it's february 29, set to 11:59 on the 28th.
+ // and it should roll over.
+ if (date_time.unit.day > days_in_month[date_time.unit.month - 1]) {
+ date_time.unit.day = 1;
+ }
+ break;
+ }
+ watch_rtc_set_date_time(date_time);
+ break;
+ case EVENT_TIMEOUT:
+ movement_move_to_face(0);
+ break;
+ default:
+ break;
+ }
+
+ char buf[11];
+ if (current_page < 3) {
+ watch_set_colon();
+ if (settings->bit.clock_mode_24h) {
+ watch_set_indicator(WATCH_INDICATOR_24H);
+ sprintf(buf, "%s %2d%02d%02d", set_time_face_titles[current_page], date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
+ } else {
+ sprintf(buf, "%s %2d%02d%02d", set_time_face_titles[current_page], (date_time.unit.hour % 12) ? (date_time.unit.hour % 12) : 12, date_time.unit.minute, date_time.unit.second);
+ if (date_time.unit.hour > 12) watch_set_indicator(WATCH_INDICATOR_PM);
+ else watch_clear_indicator(WATCH_INDICATOR_PM);
+ }
+ } else {
+ watch_clear_colon();
+ watch_clear_indicator(WATCH_INDICATOR_24H);
+ watch_clear_indicator(WATCH_INDICATOR_PM);
+ sprintf(buf, "%s %2d%02d%02d", set_time_face_titles[current_page], date_time.unit.year + 20, date_time.unit.month, date_time.unit.day);
+ }
+ if (event.subsecond % 2) {
+ switch (current_page) {
+ case 0:
+ case 3:
+ buf[4] = buf[5] = ' ';
+ break;
+ case 1:
+ case 4:
+ buf[6] = buf[7] = ' ';
+ break;
+ case 2:
+ case 5:
+ buf[8] = buf[9] = ' ';
+ break;
+ }
+ }
+
+ watch_display_string(buf, 0);
+
+ return true;
+}
+
+void set_time_face_resign(movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+ watch_set_led_off();
+ movement_request_tick_frequency(1);
+}
diff --git a/movement/watch_faces/settings/set_time_face.h b/movement/watch_faces/settings/set_time_face.h
new file mode 100644
index 00000000..21fb1e44
--- /dev/null
+++ b/movement/watch_faces/settings/set_time_face.h
@@ -0,0 +1,19 @@
+#ifndef SET_TIME_FACE_H_
+#define SET_TIME_FACE_H_
+
+#include "movement.h"
+
+void set_time_face_setup(movement_settings_t *settings, void ** context_ptr);
+void set_time_face_activate(movement_settings_t *settings, void *context);
+bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
+void set_time_face_resign(movement_settings_t *settings, void *context);
+
+static const watch_face_t set_time_face = {
+ set_time_face_setup,
+ set_time_face_activate,
+ set_time_face_loop,
+ set_time_face_resign,
+ NULL
+};
+
+#endif // SET_TIME_FACE_H_
diff --git a/movement/watch_faces/thermistor/thermistor_driver.c b/movement/watch_faces/thermistor/thermistor_driver.c
new file mode 100644
index 00000000..9e5d6fd7
--- /dev/null
+++ b/movement/watch_faces/thermistor/thermistor_driver.c
@@ -0,0 +1,76 @@
+#include "thermistor_driver.h"
+#include "watch.h"
+
+#define THERMISTOR_B_COEFFICIENT (3950.0)
+#define THERMISTOR_NOMINAL_TEMPERATURE (25.0)
+#define THERMISTOR_NOMINAL_RESISTANCE (10000.0)
+#define THERMISTOR_SERIES_RESISTANCE (10000.0)
+
+// TODO: we really need a math library.
+uint32_t msb(uint32_t v);
+double ln(double y);
+
+void thermistor_driver_enable() {
+ // Enable the ADC peripheral, which we'll use to read the thermistor value.
+ watch_enable_adc();
+ // Enable analog circuitry on pin A1, which is tied to the thermistor resistor divider.
+ watch_enable_analog_input(A1);
+ // Enable digital output on A0, which is the power to the thermistor circuit.
+ watch_enable_digital_output(A0);
+}
+
+void thermistor_driver_disable() {
+ // Enable the ADC peripheral, which we'll use to read the thermistor value.
+ watch_disable_adc();
+ // Disable analog circuitry on pin A1 to save power.
+ watch_disable_analog_input(A1);
+ // Disable A0's output circuitry.
+ watch_disable_digital_output(A0);
+}
+
+float thermistor_driver_get_temperature() {
+ // set A0 high to power the thermistor circuit.
+ watch_set_pin_level(A0, true);
+ // get the pin level
+ uint16_t val = watch_get_analog_pin_level(A1);
+ // and then set A0 low to power down the thermistor circuit.
+ watch_set_pin_level(A0, false);
+
+ double reading = (double)val;
+ reading = (1023.0 * THERMISTOR_SERIES_RESISTANCE) / (reading / 64.0);
+ reading -= THERMISTOR_SERIES_RESISTANCE;
+ reading = reading / THERMISTOR_NOMINAL_RESISTANCE;
+ reading = ln(reading);
+ reading /= THERMISTOR_B_COEFFICIENT;
+ reading += 1.0 / (THERMISTOR_NOMINAL_TEMPERATURE + 273.15);
+ reading = 1.0 / reading;
+ reading -= 273.15;
+
+ return reading;
+}
+
+uint32_t msb(uint32_t v) {
+ static const int pos[32] = {0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9};
+ v |= v >> 1;
+ v |= v >> 2;
+ v |= v >> 4;
+ v |= v >> 8;
+ v |= v >> 16;
+ v = (v >> 1) + 1;
+ return pos[(v * 0x077CB531UL) >> 27];
+}
+
+double ln(double y) {
+ int log2;
+ double divisor, x, result;
+
+ log2 = msb((int)y); // See: https://stackoverflow.com/a/4970859/6630230
+ divisor = (double)(1 << log2);
+ x = y / divisor; // normalized value between [1.0, 2.0]
+
+ result = -1.7417939 + (2.8212026 + (-1.4699568 + (0.44717955 - 0.056570851 * x) * x) * x) * x;
+ result += ((double)log2) * 0.69314718; // ln(2) = 0.69314718
+
+ return result;
+}
+
diff --git a/movement/watch_faces/thermistor/thermistor_driver.h b/movement/watch_faces/thermistor/thermistor_driver.h
new file mode 100644
index 00000000..837eb15b
--- /dev/null
+++ b/movement/watch_faces/thermistor/thermistor_driver.h
@@ -0,0 +1,13 @@
+#ifndef THERMISTOR_DRIVER_H_
+#define THERMISTOR_DRIVER_H_
+
+// NOTE: This implementation is specific to one prototype sensor board, OSO-MISC-21-009, but both
+// the sensor board design and this implementation are likely to change. Thermistor functionality
+// may even end up being baked into the Sensor Watch library. This is all by way of saying this
+// code is very temporary and the thermistor screens will likely get a rewrite in the future.
+
+void thermistor_driver_enable();
+void thermistor_driver_disable();
+float thermistor_driver_get_temperature();
+
+#endif // THERMISTOR_DRIVER_H_
diff --git a/movement/watch_faces/thermistor/thermistor_logging_face.c b/movement/watch_faces/thermistor/thermistor_logging_face.c
new file mode 100644
index 00000000..5351ce88
--- /dev/null
+++ b/movement/watch_faces/thermistor/thermistor_logging_face.c
@@ -0,0 +1,114 @@
+#include <stdlib.h>
+#include <string.h>
+#include "thermistor_logging_face.h"
+#include "thermistor_driver.h"
+#include "watch.h"
+
+void _thermistor_logging_face_log_data(thermistor_logger_state_t *logger_state) {
+ thermistor_driver_enable();
+ watch_date_time date_time = watch_rtc_get_date_time();
+ size_t pos = logger_state->data_points % THERMISTOR_LOGGING_NUM_DATA_POINTS;
+
+ logger_state->data[pos].timestamp.reg = date_time.reg;
+ logger_state->data[pos].temperature_c = thermistor_driver_get_temperature();
+ logger_state->data_points++;
+
+ thermistor_driver_disable();
+}
+
+void _thermistor_logging_face_update_display(thermistor_logger_state_t *logger_state, bool in_fahrenheit, bool clock_mode_24h) {
+ int8_t pos = (logger_state->data_points - 1 - logger_state->display_index) % THERMISTOR_LOGGING_NUM_DATA_POINTS;
+ char buf[14];
+
+ watch_clear_indicator(WATCH_INDICATOR_24H);
+ watch_clear_indicator(WATCH_INDICATOR_PM);
+ watch_clear_colon();
+
+ if (pos < 0) {
+ sprintf(buf, "TL%2dno dat", logger_state->display_index);
+ } else if (logger_state->ts_ticks) {
+ watch_date_time date_time = logger_state->data[pos].timestamp;
+ watch_set_colon();
+ if (clock_mode_24h) {
+ watch_set_indicator(WATCH_INDICATOR_24H);
+ } else {
+ if (date_time.unit.hour > 11) watch_set_indicator(WATCH_INDICATOR_PM);
+ date_time.unit.hour %= 12;
+ if (date_time.unit.hour == 0) date_time.unit.hour = 12;
+ }
+ sprintf(buf, "AT%2d%2d%02d%02d", date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
+ } else {
+ if (in_fahrenheit) {
+ sprintf(buf, "TL%2d%4.1f#F", logger_state->display_index, logger_state->data[pos].temperature_c * 1.8 + 32.0);
+ } else {
+ sprintf(buf, "TL%2d%4.1f#C", logger_state->display_index, logger_state->data[pos].temperature_c);
+ }
+ }
+
+ watch_display_string(buf, 0);
+}
+
+void thermistor_logging_face_setup(movement_settings_t *settings, void ** context_ptr) {
+ (void) settings;
+ if (*context_ptr == NULL) *context_ptr = malloc(sizeof(thermistor_logger_state_t));
+ memset(*context_ptr, 0, sizeof(thermistor_logger_state_t));
+}
+
+void thermistor_logging_face_activate(movement_settings_t *settings, void *context) {
+ (void) settings;
+ thermistor_logger_state_t *logger_state = (thermistor_logger_state_t *)context;
+ logger_state->display_index = 0;
+ logger_state->ts_ticks = 0;
+}
+
+bool thermistor_logging_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
+ thermistor_logger_state_t *logger_state = (thermistor_logger_state_t *)context;
+ switch (event.event_type) {
+ case EVENT_TIMEOUT:
+ movement_move_to_face(0);
+ break;
+ case EVENT_MODE_BUTTON_UP:
+ movement_move_to_next_face();
+ break;
+ case EVENT_LIGHT_LONG_PRESS:
+ // light button shows the timestamp, but if you need the light, long press it.
+ movement_illuminate_led();
+ break;
+ case EVENT_LIGHT_BUTTON_DOWN:
+ logger_state->ts_ticks = 2;
+ _thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h);
+ break;
+ case EVENT_ALARM_BUTTON_DOWN:
+ logger_state->display_index = (logger_state->display_index + 1) % THERMISTOR_LOGGING_NUM_DATA_POINTS;
+ logger_state->ts_ticks = 0;
+ // fall through
+ case EVENT_ACTIVATE:
+ _thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h);
+ break;
+ case EVENT_TICK:
+ if (logger_state->ts_ticks && --logger_state->ts_ticks == 0) {
+ _thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h);
+ }
+ break;
+ case EVENT_BACKGROUND_TASK:
+ _thermistor_logging_face_log_data(logger_state);
+ break;
+ default:
+ break;
+ }
+
+ return true;
+}
+
+void thermistor_logging_face_resign(movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+}
+
+bool thermistor_logging_face_wants_background_task(movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+ // this will get called at the top of each minute, so all we check is if we're at the top of the hour as well.
+ // if we are, we ask for a background task.
+ return watch_rtc_get_date_time().unit.minute == 0;
+}
diff --git a/movement/watch_faces/thermistor/thermistor_logging_face.h b/movement/watch_faces/thermistor/thermistor_logging_face.h
new file mode 100644
index 00000000..ece89396
--- /dev/null
+++ b/movement/watch_faces/thermistor/thermistor_logging_face.h
@@ -0,0 +1,35 @@
+#ifndef THERMISTOR_LOGGING_FACE_H_
+#define THERMISTOR_LOGGING_FACE_H_
+
+#include "movement.h"
+#include "watch.h"
+
+#define THERMISTOR_LOGGING_NUM_DATA_POINTS (36)
+
+typedef struct {
+ watch_date_time timestamp;
+ float temperature_c;
+} thermistor_logger_data_point_t;
+
+typedef struct {
+ uint8_t display_index; // the index we are displaying on screen
+ uint8_t ts_ticks; // when the user taps the LIGHT button, we show the timestamp for a few ticks.
+ int32_t data_points; // the absolute number of data points logged
+ thermistor_logger_data_point_t data[THERMISTOR_LOGGING_NUM_DATA_POINTS];
+} thermistor_logger_state_t;
+
+void thermistor_logging_face_setup(movement_settings_t *settings, void ** context_ptr);
+void thermistor_logging_face_activate(movement_settings_t *settings, void *context);
+bool thermistor_logging_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
+void thermistor_logging_face_resign(movement_settings_t *settings, void *context);
+bool thermistor_logging_face_wants_background_task(movement_settings_t *settings, void *context);
+
+static const watch_face_t thermistor_logging_face = {
+ thermistor_logging_face_setup,
+ thermistor_logging_face_activate,
+ thermistor_logging_face_loop,
+ thermistor_logging_face_resign,
+ thermistor_logging_face_wants_background_task
+};
+
+#endif // THERMISTOR_LOGGING_FACE_H_
diff --git a/movement/watch_faces/thermistor/thermistor_readout_face.c b/movement/watch_faces/thermistor/thermistor_readout_face.c
new file mode 100644
index 00000000..5cee2e96
--- /dev/null
+++ b/movement/watch_faces/thermistor/thermistor_readout_face.c
@@ -0,0 +1,73 @@
+#include <stdlib.h>
+#include <string.h>
+#include "thermistor_readout_face.h"
+#include "thermistor_driver.h"
+#include "watch.h"
+
+void _thermistor_readout_face_update_display(bool in_fahrenheit) {
+ thermistor_driver_enable();
+ float temperature_c = thermistor_driver_get_temperature();
+ char buf[14];
+ if (in_fahrenheit) {
+ sprintf(buf, "%4.1f#F", temperature_c * 1.8 + 32.0);
+ } else {
+ sprintf(buf, "%4.1f#C", temperature_c);
+ }
+ watch_display_string(buf, 4);
+ thermistor_driver_disable();
+}
+
+void thermistor_readout_face_setup(movement_settings_t *settings, void ** context_ptr) {
+ (void) settings;
+ (void) context_ptr;
+}
+
+void thermistor_readout_face_activate(movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+ watch_display_string("TE", 0);
+}
+
+bool thermistor_readout_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
+ (void) context;
+ watch_date_time date_time = watch_rtc_get_date_time();
+ switch (event.event_type) {
+ case EVENT_MODE_BUTTON_UP:
+ movement_move_to_next_face();
+ break;
+ case EVENT_LIGHT_BUTTON_DOWN:
+ movement_illuminate_led();
+ break;
+ case EVENT_ALARM_BUTTON_UP:
+ settings->bit.use_imperial_units = !settings->bit.use_imperial_units;
+ _thermistor_readout_face_update_display(settings->bit.use_imperial_units);
+ break;
+ case EVENT_ACTIVATE:
+ // force a measurement to be taken immediately.
+ date_time.unit.second = 0;
+ // fall through
+ case EVENT_TICK:
+ if (date_time.unit.second % 5 == 4) {
+ // Not 100% on this, but I like the idea of using the signal indicator to indicate that we're sensing data.
+ // In this case we turn the indicator on a second before the reading is taken, and clear it when we're done.
+ // In reality the measurement takes a fraction of a second, but this is just to show something is happening.
+ watch_set_indicator(WATCH_INDICATOR_SIGNAL);
+ } else if (date_time.unit.second % 5 == 0) {
+ _thermistor_readout_face_update_display(settings->bit.use_imperial_units);
+ watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
+ }
+ break;
+ case EVENT_TIMEOUT:
+ movement_move_to_face(0);
+ break;
+ default:
+ break;
+ }
+
+ return true;
+}
+
+void thermistor_readout_face_resign(movement_settings_t *settings, void *context) {
+ (void) settings;
+ (void) context;
+}
diff --git a/movement/watch_faces/thermistor/thermistor_readout_face.h b/movement/watch_faces/thermistor/thermistor_readout_face.h
new file mode 100644
index 00000000..71d15427
--- /dev/null
+++ b/movement/watch_faces/thermistor/thermistor_readout_face.h
@@ -0,0 +1,19 @@
+#ifndef THERMISTOR_READOUT_FACE_H_
+#define THERMISTOR_READOUT_FACE_H_
+
+#include "movement.h"
+
+void thermistor_readout_face_setup(movement_settings_t *settings, void ** context_ptr);
+void thermistor_readout_face_activate(movement_settings_t *settings, void *context);
+bool thermistor_readout_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
+void thermistor_readout_face_resign(movement_settings_t *settings, void *context);
+
+static const watch_face_t thermistor_readout_face = {
+ thermistor_readout_face_setup,
+ thermistor_readout_face_activate,
+ thermistor_readout_face_loop,
+ thermistor_readout_face_resign,
+ NULL
+};
+
+#endif // THERMISTOR_READOUT_FACE_H_