summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjoeycastillo <joeycastillo@utexas.edu>2021-10-18 15:37:41 -0400
committerGitHub <noreply@github.com>2021-10-18 15:37:41 -0400
commit6210e1c233a3ee66274389e85889b0681102378d (patch)
treeec94a012f533e8f7017223648321c46c3c9796f4
parent84c0fbfa2a68dc3c29b989947567d111e70a037b (diff)
parent32710098577c982123d9391132165c0b02a57482 (diff)
downloadSensor-Watch-6210e1c233a3ee66274389e85889b0681102378d.tar.gz
Sensor-Watch-6210e1c233a3ee66274389e85889b0681102378d.tar.bz2
Sensor-Watch-6210e1c233a3ee66274389e85889b0681102378d.zip
Merge pull request #17 from joeycastillo/wip-launcher
Resolves #13
-rw-r--r--apps/Sensor Watch Starter Project/app.c2
-rw-r--r--movement/README.md297
-rwxr-xr-xmovement/make/.gitignore1
-rwxr-xr-xmovement/make/Makefile32
-rw-r--r--movement/movement.c226
-rw-r--r--movement/movement.h181
-rw-r--r--movement/movement_config.h17
-rw-r--r--movement/watch_faces/clock/simple_clock_face.c92
-rw-r--r--movement/watch_faces/clock/simple_clock_face.h21
-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/settings/preferences_face.c122
-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--watch-library/watch/watch_deepsleep.c7
-rw-r--r--watch-library/watch/watch_deepsleep.h15
-rw-r--r--watch-library/watch/watch_slcd.c25
-rw-r--r--watch-library/watch/watch_slcd.h4
19 files changed, 1286 insertions, 21 deletions
diff --git a/apps/Sensor Watch Starter Project/app.c b/apps/Sensor Watch Starter Project/app.c
index ff5ed53d..ae5aa6e8 100644
--- a/apps/Sensor Watch Starter Project/app.c
+++ b/apps/Sensor Watch Starter Project/app.c
@@ -157,7 +157,7 @@ bool app_loop() {
delay_ms(250);
// nap time :)
- watch_enter_shallow_sleep(NULL);
+ watch_enter_shallow_sleep(false);
// we just woke up; wait a moment again for the user's finger to be off the button...
delay_ms(250);
diff --git a/movement/README.md b/movement/README.md
new file mode 100644
index 00000000..e4eeb1c4
--- /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 releasing 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_UP:
+ 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..f17fc7e5
--- /dev/null
+++ b/movement/make/Makefile
@@ -0,0 +1,32 @@
+# 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/ \
+
+# 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 \
+
+# 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..8d28cc55
--- /dev/null
+++ b/movement/movement.c
@@ -0,0 +1,226 @@
+#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_inactivity_interval];
+ movement_state.timeout_ticks = movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_inactivity_interval];
+}
+
+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() {
+ movement_state.light_ticks = movement_state.settings.bit.led_duration;
+}
+
+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_inactivity_interval = 1;
+ movement_state.settings.bit.led_duration = 3;
+ _movement_reset_inactivity_countdown();
+}
+
+void app_wake_from_deep_sleep() {
+ // This app does not support deep sleep 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;
+ }
+ }
+ if (movement_state.le_mode_ticks != -1) {
+ watch_disable_extwake_interrupt(BTN_ALARM);
+ watch_rtc_disable_alarm_callback();
+
+ 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_sleep() {
+}
+
+void app_wake_from_sleep() {
+}
+
+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 off and should be on, turn it on
+ if (movement_state.light_ticks > 0 && !movement_state.led_on) {
+ 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;
+
+ }
+
+ // 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;
+ }
+
+ // 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_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);
+ 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) {
+ 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_shallow_sleep(true);
+ }
+ // 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 shallow sleep, 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() {
+ event.event_type = EVENT_LOW_ENERGY_UPDATE;
+}
+
+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) {
+ if (movement_state.light_ticks) movement_state.light_ticks--;
+ if (movement_state.settings.bit.le_inactivity_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..d116a6e0
--- /dev/null
+++ b/movement/movement.h
@@ -0,0 +1,181 @@
+#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 clock_mode_24h : 1; // determines whether clock should use 12 or 24 hour mode.
+ uint32_t button_should_sound : 1; // if true, pressing a button emits a sound.
+ uint32_t to_inactivity_interval : 2;// an inactivity interval for asking the active face to resign.
+ uint32_t le_inactivity_interval : 3;// 0 to disable low energy mode, or an inactivity interval for going into low energy mode.
+ uint32_t led_duration : 3; // how many seconds to shine the LED for, 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)
+ } 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;
+
+ // 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..6a7aec5a
--- /dev/null
+++ b/movement/movement_config.h
@@ -0,0 +1,17 @@
+#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"
+
+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..95db3901
--- /dev/null
+++ b/movement/watch_faces/clock/simple_clock_face.c
@@ -0,0 +1,92 @@
+#include <stdlib.h>
+#include "simple_clock_face.h"
+#include "watch.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 (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) {
+ const char weekdays[7][3] = {"SA", "SU", "MO", "TU", "WE", "TH", "FR"};
+ 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) {
+ sprintf(buf, "%s%2d%2d%02d ", weekdays[simple_clock_face_get_weekday(date_time.unit.year, date_time.unit.month, date_time.unit.day)], date_time.unit.day, date_time.unit.hour, date_time.unit.minute);
+ } else {
+ sprintf(buf, "%s%2d%2d%02d%02d", weekdays[simple_clock_face_get_weekday(date_time.unit.year, date_time.unit.month, date_time.unit.day)], 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_UP:
+ 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;
+}
+
+uint8_t simple_clock_face_get_weekday(uint16_t year, uint16_t month, uint16_t day) {
+ year += 20;
+ if (month <= 2) {
+ month += 12;
+ year--;
+ }
+ return (day + 13 * (month + 1) / 5 + year + year / 4 + 525) % 7;
+}
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/pulsometer_face.c b/movement/watch_faces/complications/pulsometer_face.c
new file mode 100644
index 00000000..abe002fb
--- /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_UP:
+ 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/settings/preferences_face.c b/movement/watch_faces/settings/preferences_face.c
new file mode 100644
index 00000000..98a2372d
--- /dev/null
+++ b/movement/watch_faces/settings/preferences_face.c
@@ -0,0 +1,122 @@
+#include <stdlib.h>
+#include "preferences_face.h"
+#include "watch.h"
+
+#define PREFERENCES_FACE_NUM_PREFEFENCES (5)
+const char preferences_face_titles[PREFERENCES_FACE_NUM_PREFEFENCES][11] = {"CL ", "Bt Beep ", "SC ", "Lt grn ", "Lt red "};
+
+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.le_inactivity_interval = settings->bit.le_inactivity_interval + 1;
+ break;
+ case 3:
+ settings->bit.led_green_color = settings->bit.led_green_color + 1;
+ break;
+ case 4:
+ 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);
+
+ if (event.subsecond % 2) return current_page <= 2;
+ char buf[3];
+ 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.le_inactivity_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 3:
+ sprintf(buf, "%2d", settings->bit.led_green_color);
+ watch_display_string(buf, 8);
+ break;
+ case 4:
+ sprintf(buf, "%2d", settings->bit.led_red_color);
+ watch_display_string(buf, 8);
+ break;
+ }
+
+ if (current_page > 2) {
+ 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;
+ }
+
+ 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/watch-library/watch/watch_deepsleep.c b/watch-library/watch/watch_deepsleep.c
index b7da82ee..9ca53db0 100644
--- a/watch-library/watch/watch_deepsleep.c
+++ b/watch-library/watch/watch_deepsleep.c
@@ -151,11 +151,8 @@ void _watch_disable_all_peripherals_except_slcd() {
MCLK->APBCMASK.reg &= ~MCLK_APBCMASK_SERCOM3;
}
-void watch_enter_shallow_sleep(char *message) {
- if (message != NULL) {
- watch_display_string(" ", 0);
- watch_display_string(message, 0);
- } else {
+void watch_enter_shallow_sleep(bool display_on) {
+ if (!display_on) {
slcd_sync_deinit(&SEGMENT_LCD_0);
hri_mclk_clear_APBCMASK_SLCD_bit(SLCD);
}
diff --git a/watch-library/watch/watch_deepsleep.h b/watch-library/watch/watch_deepsleep.h
index 3dc428d0..84825f00 100644
--- a/watch-library/watch/watch_deepsleep.h
+++ b/watch-library/watch/watch_deepsleep.h
@@ -81,13 +81,8 @@ uint32_t watch_get_backup_data(uint8_t reg);
* the LCD. You can wake from this mode by pressing the ALARM button, if you have an registered an
* external wake callback on the ALARM button. When your app wakes from this shallow sleep mode, your
* app_setup method will be called, since this function will have disabled things you set up.
- * @param message Either NULL, or a string representing a message to display while in shallow sleep mode. If
- * this parameter is NULL, the screen will be blanked out, and this function will disable the
- * SLCD peripheral for additional power savings. If the message is non-NULL, it will replace
- * any text on the screen, and will be displayed at position 0 (so you should pad out the beginning
- * of the string with spaces if you wish for the message to appear on line 2, i.e. " SLEEP").
- * Also note that this function will NOT clear any indicator segments that you have set. This is
- * by design, in case you wish to leave an indicator lit in sleep mode.
+ * @param display_on if true, leaves the LCD on to display whatever content was on-screen. If false, disables
+ * the segment LCD controller for additional power savings.
* @details This shallow sleep mode is not the lowest power mode available (see watch_enter_deep_sleep), but
* it has the benefit of retaining your application state and being able to wake from the ALARM button.
* It also provides an option for displaying a message to the user when asleep. Note that whether you
@@ -96,10 +91,10 @@ uint32_t watch_get_backup_data(uint8_t reg);
*
* Power consumption in shallow sleep mode varies a bit with the battery voltage and the temperature,
* but at 3 V and 25~30° C you can roughly estimate:
- * * < 12µA current draw with the LCD controller on (message != NULL)
- * * < 6µA current draw with the LCD controller off (message == NULL)
+ * * < 12µA current draw with the LCD controller on
+ * * < 6µA current draw with the LCD controller off
*/
-void watch_enter_shallow_sleep(char *message);
+void watch_enter_shallow_sleep(bool display_on);
/** @brief Enters the SAM L22's lowest-power mode, BACKUP.
* @details This function does some housekeeping before entering BACKUP mode. It first disables all
diff --git a/watch-library/watch/watch_slcd.c b/watch-library/watch/watch_slcd.c
index 58e1da7f..08f8c0e3 100644
--- a/watch-library/watch/watch_slcd.c
+++ b/watch-library/watch/watch_slcd.c
@@ -115,8 +115,8 @@ static const uint8_t Character_Set[] =
0b01010000, // r
0b01101101, // s
0b01111000, // t
- 0b01100010, // u (appears as superscript to work in more positions)
- 0b01100010, // v (appears as superscript to work in more positions)
+ 0b01100010, // u (appears in (u)pper half to work in more positions)
+ 0b00011100, // v (looks like u but in the lower half)
0b10111110, // w (only works in position 0)
0b01111110, // x
0b01101110, // y
@@ -167,9 +167,23 @@ inline void watch_clear_pixel(uint8_t com, uint8_t seg) {
slcd_sync_seg_off(&SEGMENT_LCD_0, SLCD_SEGID(com, seg));
}
+void watch_clear_display() {
+ SLCD->SDATAL0.reg = 0;
+ SLCD->SDATAL1.reg = 0;
+ SLCD->SDATAL2.reg = 0;
+}
+
void watch_display_character(uint8_t character, uint8_t position) {
- // handle lowercase 7 if needed
- if (character == '7' && (position == 4 || position == 6)) character = '&';
+ // special cases for positions 4 and 6
+ if (position == 4 || position == 6) {
+ if (character == '7') character = '&'; // "lowercase" 7
+ if (character == 'v') character = 'u'; // bottom segment duplicated, so show in top half
+ if (character == 'J') character = 'j'; // same
+ } else if (position != 4 && position != 6) {
+ if (character == 'u') character = 'v'; // we can use the bottom segment; move to lower half
+ if (character == 'j') character = 'J'; // same but just display a normal J
+ }
+ if (position == 0) slcd_sync_seg_off(&SEGMENT_LCD_0, SLCD_SEGID(0, 15)); // clear funky ninth segment
uint64_t segmap = Segment_Map[position];
uint64_t segdata = Character_Set[character - 0x20];
@@ -188,7 +202,8 @@ void watch_display_character(uint8_t character, uint8_t position) {
segmap = segmap >> 8;
segdata = segdata >> 1;
}
- if (character == 'T' && position == 1) slcd_sync_seg_on(&SEGMENT_LCD_0, SLCD_SEGID(1, 12));
+ if (character == 'T' && position == 1) slcd_sync_seg_on(&SEGMENT_LCD_0, SLCD_SEGID(1, 12)); // add descender
+ else if (position == 0 && (character == 'B' || character == 'D')) slcd_sync_seg_on(&SEGMENT_LCD_0, SLCD_SEGID(0, 15)); // add funky ninth segment
}
void watch_display_string(char *string, uint8_t position) {
diff --git a/watch-library/watch/watch_slcd.h b/watch-library/watch/watch_slcd.h
index 4e710936..e18ee9b4 100644
--- a/watch-library/watch/watch_slcd.h
+++ b/watch-library/watch/watch_slcd.h
@@ -69,6 +69,10 @@ void watch_set_pixel(uint8_t com, uint8_t seg);
*/
void watch_clear_pixel(uint8_t com, uint8_t seg);
+/** @brief Clears all segments of the display, including incicators and the colon.
+ */
+void watch_clear_display();
+
/** @brief Displays a string at the given position, starting from the top left. There are ten digits.
A space in any position will clear that digit.
* @param string A null-terminated string.