sensor-watch/movement
Matheus Afonso Martins Moreira 3c86a42aa8 Merge PR #470 - fixes world_clock2 face
The new DST changes caused problems in one specific face - world_clock2.
An incorrect variable was used due to a confusing name.
It has been revised to fix the problems that were caused.

Closes #475.

Reported-by: CarpeNoctem <cryptomax@pm.me>
Fixed-by: David Volovskiy <devolov@gmail.com>
Tested-by: CarpeNoctem <cryptomax@pm.me>
Tested-on-hardware-by: CarpeNoctem <cryptomax@pm.me>
GitHub-Pull-Request: https://github.com/joeycastillo/Sensor-Watch/pull/470
GitHub-Issue: https://github.com/joeycastillo/Sensor-Watch/issues/475
2024-09-15 20:45:33 -03:00
..
alt_fw update alternate firmware for new board color 2023-09-13 14:08:52 -04:00
lib Reduce totp_face_lfs memory usage 2024-04-26 16:05:35 +01:00
make make: add beeps face to build 2024-09-10 00:05:54 -03:00
template template: fix compiler warning on watch_face_index as mentioned in PR 269 2024-01-17 23:08:54 +11:00
watch_faces Merge PR #470 - fixes world_clock2 face 2024-09-15 20:45:33 -03:00
filesystem.c explain usage in format command when arg isn't YES 2024-08-05 23:11:54 +00:00
filesystem.h add a format command 2024-08-05 23:06:18 +00:00
movement.c Revert PR #387 - fixes LE state restoration 2024-09-10 12:10:48 -03:00
movement.h Revert PR #387 - fixes LE state restoration 2024-09-10 12:10:48 -03:00
movement_config.h Revert PR #387 - fixes LE state restoration 2024-09-10 12:10:48 -03:00
movement_custom_signal_tunes.h Merge PR #465 - add metal gear solid codec chime 2024-09-07 16:43:00 -03:00
movement_faces.h Merge PR #401 - add fuzzy time watch face 2024-09-07 20:57:24 -03:00
README.md movement: add documentation mentioned in #42 2022-01-25 17:36:04 -05:00
shell.c Fix compile errors and warnings in movement.c and shell.c 2024-03-29 11:49:48 +01:00
shell.h USB Improvements 2024-01-07 00:20:20 -05:00
shell_cmd_list.c add a format command 2024-08-05 23:06:18 +00:00
shell_cmd_list.h USB Improvements 2024-01-07 00:20:20 -05:00

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, will be added to the guide at a later date. You may omit it.

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:

#define moon_phase_face ((const watch_face_t){ \
    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 */ \
})

You will also have to add your watch face to the Makefile so that it will be compiled in, and to movement_faces.h so that it will be available to add to the carousel. A good example of the changes required can be found here.

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 64. (there is a 128 Hz prescaler tick, but Movement reserves that for its own use)

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 very good reason to override this behavior (e.g. your user interface requires all three buttons), you may do so, but the user will have to long-press the Mode button to advance to the next watch face.

Note that watch_face_loop returns a boolean value. This boolean value indicates to Movement whether the watch can enter standby mode after handling your loop (true), or whether it should stay awake (false). You SHOULD almost always return true here, as the watch uses significantly more power when idling as opposed to standing by. The only times you would return false here are if you are PWM'ing the LED or emitting a sound from the buzzer. Your watch face would want to keep the watch awake in this case because the PWM driver does not run in standby.

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. 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 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:

#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:

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:

void pulsometer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, 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);

#define pulsometer_face ((const watch_face_t){ \
    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:

#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

void pulsometer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, 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

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:

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.

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.

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.

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.

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.

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.

    } 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.

case EVENT_TIMEOUT:
    movement_move_to_face(0);
    break;

Watch Face Resignation

The resign function doesn't have anything to do; it just has to be there.

void pulsometer_face_resign(movement_settings_t *settings, void *context) {
    (void) settings;
    (void) context;
}

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.