Screen based implementation of the Watchy library. It assumes some familiarity with the Watchy library and only documents the differences.
To build this project, install PlatformIO and simply run pio run -t upload
to build and upload to the watch.
class Screen {
public:
Screen* parent;
uint16_t bgColor;
Screen(uint16_t bg = GxEPD_BLACK) : bgColor(bg){};
virtual void show() = 0;
virtual void up() {}
virtual void down() {}
virtual void back() { Watchy::setScreen(parent); }
virtual void menu() {}
};};
A screen encapsulates the logic to display one screenful of information, and to respond to button presses. To make a Screen
instance, subclass Screen
and implement the show
method to display your screen. show
is called with the screen cleared to your bgColor
, text color set to the opposite of bgColor
, and the cursor set to 0,0. After show
is done painting, it should return and the display will do a partial refresh.
By default the screen does nothing on up, down, or menu button press, and goes up to it's parent (if any) on back button press. To enable the screen to respond to buttons override the corresponding button method in your subclass. For example, if there was a screen to show the time called timeScreen
and another to display a content called contentScreen
Where pressing the up
button while displaying the time screen should display the content, and pressing Back
while displaying the content screen should return to the time screen:
// ContentScreen.h
#include "Screen.h"
#include "TimeScreen.h"
#include "Watchy.h"
class ContentScreen : public Screen {
public:
void show() { Watchy::display.print("This is the content."); }
void back() { Watchy::setScreen(&timeScreen); }
}
ContentScreen contentScreen;
//TimeScreen.h
#include "ContentScreen.h"
#include "Screen.h"
#include "Watchy.h"
class TimeScreen : public Screen {
public:
void show() { Watchy::display.print("This is the time."); }
void menu() { Watchy::setScreen(&contentScreen); }
}
TimeScreen timeScreen;
And in the main program, make TimeScreen
the initial screen on startup
// main.cpp
#include "TimeScreen.h"
#include "Watchy.h" //include the Watchy library
void setup() {
if (Watchy::screen == nullptr) { Watchy::screen = &timeScreen; }
Watchy::init();
}
void loop() {} // this should never run, Watchy deep sleeps after init();
Manually coding the relationships between screens is tedious and error prone though. See Menu
and Carousel
below for a nicer approach.
- Watchy is no longer a class, you don't have to instantiate it. Now it's a namespace, which means that any references to Watchy need to be prefixed with
Watchy::
- You need to call
Watchy::init
from your setup function. - You need to initialize
Watchy::screen
on initial boot before callingWatchy::init
(it's persistent after that)
- Use
Watchy::setScreen(Screen *s)
to change screens. - Use
Watchy::showWatchFace(bool partialRefresh)
to redisplay the current screen.
There are some helper functions if you want to do polling UI instead of sleeping and waiting for an event. Normally you should just use the event driven UI, but look at SetTime.{h,cpp}
and UpdateFW.{h,cpp}
for examples of how to implement a captive UI. Orientation.{h,cpp}
shows how to refresh the display more frequently than once a minute. Unfortunately all of those use a traditional polling loop, which is energy intensive. Eventually they should all be event driven.
Because of how deep sleep works, all screen instances must be globally persistent. The easiest way is to make each screen a global instance and let class initialization set them up. In particular the heap is not preserved across deep sleep, so you probably can't easily dynamically allocate screens.
The fields in a screen instance are not persistent across deep sleep. Do not expect to store persistent state in a screen instance. If you need persistence across sleep, make the state static and tag it with RTC_DATA_ATTR
but this will only work for a single instance of a screen. If you have multiple instances of the same screen that need different persistent instance data you'll have do it some other way.
If you want multiple sub-screens for a single action you can instantiate local anonymous subclasses of Screen
see UpdateFW.cpp
for an example.
The Screens
directory includes individual screens for:
- Time/Date
- Weather
- Battery
- Bluetooth Status
- Orientation
- Set Time - Set the current time and date
- Setup Wifi - Sets up Wifi credentials
- Update Firmware - Update the watch firmware Over-The-Air via bluetooth
- Number of Steps
- Wifi Status
And two "aggregate" screens used to organize other screens:
- Menu - Simple scrollable text menu.
- Carousel - A list of "splash" screens and their corresponding display screens.
The menu screen implements a scrollable text menu. It takes an array of menu items, each consisting of a textual menu item name and corresponding screen to invoke. Adding new items to the menu is as simple as adding another MenuItem
to the array in the call to the constructor. up
and down
navigate the menu, menu
invokes the current item. When in a menu item's screen back
takes you back to the menu. If there are more items than can be displayed, the menu will show scroll arrows at the top or bottom of the screen to indicate there are more items.
The carousel implements a cyclic view of spash screens and a corresponding display. Typically a splash screen is a simple static graphical or textual screen, and a display screen has more complex UI and active logic. To construct a carousel create an array of CarouselItems
each one is a pair of screens the splash
that is displayed in the carousel and the optional child
that is displayed when you press Menu
on the splash screen. The child
screen is free to use any of the buttons for its own purposes, though conventionally the back
button should return to the splash screen.
In a carousel up
takes you to the previous splash screen, down
takes you to the next one, menu
shows the child screen for that splash screen, back
while looking at a splash screen takes you back to the first screen of the carousel. A splash screen uses all of the buttons for its UI. In a child screen the child has complete control of the buttons, though by convention back
should take you back to the carousel (the parent
of the child screen). If the splash screen contains all information for the display and doesn't need any additional UI, the child screen can be null. To add another screen to a carousel, add another CarouselItem
to the array passed to the Carousel
constructor in the location you want the screen to appear in the carousel.
The face is organized as a carousel of time, weather, battery, steps, orientation, bluetooth status, weather status, and a settings menu. The time and weather screens are standalone, they don't have a child display associated.
The settings display screen is a menu that lets you set the time, setup the wifi, update the firmware, synchronize the time with NTP, get your location by IP geolocation, refresh the weather, or buzz the motor.
There are no dependencies between the screens, all of the screen instances and their organization will be in main.cpp
most of the screens are just a show
method.
See the file PORTING.md
for information on how to port a watchface from Watchy to Watchy-Screen.