PineTime Custom Watchface Tutorial
PineTime Custom Watchface Tutorial
This is a tutorial to help new users create custom watchfaces based on the InfiniTime Firmware for Pinetime made by user JF002, thanks to him for its development...
We will explain some of the things we went through while creating some custom Watchfaces, so consider this as a log of experiments of sorts..
So stay tuned to it as it will be dynamically updated...
What you need to start
The entire building process will be done by GitHub, so all you need is a device which can give you a Github Web Client, a PC or tablet to give you enough screen space to review your code and a steady internet connection.
Since the compiling and file management is done by Github online, you have nothing else to worry about other than working with the files that display the watch face.
So with those things settled, let's start with the basics of a watchface.
Please remember that this is a wiki, so you can make an account and help us improve this page.
Please make sure not to unilaterally remove info though, but offer an alternative. If it is indeed a better way, in time your alternative will grow into the main text, and the latter info will be pruned.
Overview
The Firmware (also called InfiniTime) we will be working with is made with a programming language named C++:[1]
Basic knowledge of C/C++ is required to to understand the advanced watch faces as that requires more complex code, but you can still do a some cool things without much knowledge of C++ programming, just some small edits to existing programs.
InfiniTime uses the LVGL:[2] graphics library to provide users with a simple and clean UI without overpowering the Nordic nRF52832:[3] microcontroller which is the brain of the watch.
To get the watchface to work there are these basic steps. We will go over each step separately, so don't be daunted, all will become clear soon.
For now we will, modify the the existing watchface, change the positioning of the text labels, add an icon to an existing watchface, and later on we will do a full watchface.
The main file we need to focus on is the clock.cpp file, it is what contains most of the data attributed to what we see on the watchface, including The time/day characters, the battery and bluetooth icons, and also pedometer count...
so everything we will be doing in the basic modifications is purely messing with this single file.
Labels
What are labels?
Labels are considered as "elemental" parts that make up a screen's Text-based UI by the LVGL library... Each label is also considered as an "object" or "obj" and can be manipulated, by changing the data attributed with the "obj", for example, position, internal data like the strings/text etc. when can change what the label shows and where it shows it...
Modifying label data
let's observe something small like the word "BPM" near the bottom of the watch face...
If we take a look at the source file of the watchface, (a.k.a the clock.cpp file) we can observe that these particular lines are what attributes to the word "BPM being displayed...
heartbeatBpm = lv_label_create(lv_scr_act(), NULL); lv_label_set_text(heartbeatBpm, "BPM");
we can modify the text inside the quotes and replace the word 'BPM' between those quotes to something like 'LOVE' and the result after compiling the firmware again with the changes and flashing it to the watch would be that the text changes on the watch face and displays 'LOVE' inplace of 'BPM'...
If this happened correctly, then you have successfully made a custom new watchface! Now we can do something a bit more complex...
Now take for in instance the days of the week that we have on the bottom line with the date...
they are stored a "C array" which is basically multiple words separated by commas.. the date is in a format of <day of the week> <day> <Month> <year>
which in the source file is expressed as,
sprintf(dateStr, "%s %d %s %d", DayOfWeekToString(dayOfWeek), day, MonthToString(month), year); lv_label_set_text(label_date, dateStr);
here,
%s %d %s %d
stores the print format of the variables, and
DayOfWeekToString(dayOfWeek), day, MonthToString(month), year
are the variables themselves...
Now, if the date was a saturday 11/7/2020, you can observe that the date looks like
SATURDAY 11 JUL 2020
as seen in the above image (the one where we changed BPM to LOVE)
by modifying the format of the variables, we can change how those words are arranged, and add some extra characters if we like...
(there is a catch to the list of characters you can use however, but it will be discussed later...)
For example,
We modifying the format by adding a comma... "," in between the second "%s" and "%d" like this,
"%s %d %s, %d"
and changing that in the line...
sprintf(dateStr, "%s %d %s, %d", DayOfWeekToString(dayOfWeek), day, MonthToString(month), year);
we can make the date become...
SATURDAY 11 JUL, 2020
which means we now were able to modify how the text got display to make it a bit more nice...
but if you haven't noticed, the line containing the date is already full, meaning we will get some problems while displaying the date and causing it to wrap around, making a single character go to the next line and look more like,
SATURDAY 11 JUL, 202 0
so why don't we shorten the characters present in the date from being "SATURDAY" to simply just "sat." (It will have the small period at the end, and is only 3 characters long) I will also convert the months of the year from Capital to small letters...
for that look into the part where the days of the week of are stored as text, and also while looking at it, we can solve another question, why was their two variables in the date format that looked like, DayOfWeekToString(dayOfWeek), and MonthToString(month) ?
It is because the system gives the date/ time as numbers (Monday-1, Tuesday-2 Wednesday-3 for the days, and 1-January, 2-February, 3-March ), and so a function along with a C array is used to assign these numbers to Days/Months in text form as it is easier to read...
this is the Array containing the day of the week, (as text)
char const *Clock::DaysString[] = { "", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY" };
and this Array stores the months of the year, (as text)
char const *Clock::MonthsString[] = { "", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };
here we can see that the days are stored in a full format as "SUNDAY", "MONDAY", "TUESDAY" etc. we can change all of them to a shorter format like "sun.", "mon.", "tue.", to make it short and nice... while doing so, we can even make the months use small letters as said before..
so the source file (clock.cpp) becomes,
(for the days of the week)
char const *Clock::DaysString[] = { "", "mon.", "tue.", "wed.", "thu.", "fri.", "sat.", "sun." };
and
(for the months of the year)
char const *Clock::MonthsString[] = { "", "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec" };
which means now our original date, saturday 11/7/2020 will become...
sat. 11 jul, 2020
you now know how to change the data present in a label object, and the format of it..,
Here is a fun idea you can try: you can even replace the days with whatever thing that tells you (or) reminds you the day of the week (like the food served in the cafe, Monday/taco, Tuesday/burger, Wednesday/pasta etc.)
NOTE: when making the custom array, don't forget to leave a empty "" as the first element of the array, This is because the date is given by the system in a natural numbers format (1,2,3...) rather than a zero-starting format (0,1,2,3...), which the C array uses to index... so the C array indexes the days as ""-0, "Monday"-1, "Tuesday"-2 etc. and the months as ""-0, "January"-1, "February"-2 etc.)
Label positioning
The locational placement in LVGL is done on a cartesian plane, where each object can have dynamic origin placement, and the Y-axis is inverted... So going down is done with a positive Y-axis value and not negative as the it is by de-facto...
The position of the various objects in clock.cpp are set by the line,
lv_obj_set_pos(<obj>, <new_x>, <new_y>)
and the top left corner is the Cartesian origin, aka coordinates (0,0)
we use another function, that is more advanced that gives the positional alignment based on preset locations...
lv_obj_align(<text/obj>, <image/obj>, <location_parameter>, <new_x>, <new_y>);
the "<image/obj>" data is used for putting text beside other picture icons , and so if using only text based labels, we can substitute it with,
lv_scr_act() (or) NULL
<new_x>, <new_y> are the cartesian coordinates, <location parameter> is replaced by LV_ALIGN_<preset>, there will be a picture showing the presets below, and <text/obj> is your text label...
here, LV_ALIGN_<preset> sets the cartesian origin...
Label positioning based on alignment is both a simple and complicated thing to understand, so here I have given something you can refer to while modifying the position of the various labels and objects...
you can also refer here for LVGL's documentation of coordinate system https://docs.lvgl.io/v6/en/html/object-types/obj.html#coordinates
It is however recommended that you use the first method to set the location
lv_obj_set_pos(<obj>, <new_x>, <new_y>)
as it is simple and easier for beginners
Here is a small example.
Take the Label that tells the date, In the source file (clock.cpp) it is this line,
lv_obj_align(label_date, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 60);
by increasing the Value of the Y coordinate (60) to a higher value, we can bring the position of the Date downwards a bit away from the Time, and toward the Heartbeat count in the bottom row here I will increase it to 80, so it becomes..
lv_obj_align(label_date, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 80);
and now we have made some space up top..
now let's try something a bit complex,
Take the position argument for the label that tells you time... here, in the source file (clock.cpp),
lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 0);
this line determines the position of the Label telling time, as seen in the image...
we modifying this, by changing the origin <preset> parameter (here it is LV_ALIGN_IN_LEFT_MID) to LV_ALIGN_IN_TOP_LEFT
you can alternatively swap the whole line to,
lv_obj_set_pos(label_time, 0, 0);
this makes the Time label/obj. to go to the top left corner...
but I will do something a little extra, I will modify the label that store the data and Time format, i.e this line,
sprintf(timeStr, "%c%c:%c%c", hoursChar[0],hoursChar[1],minutesChar[0], minutesChar[1]);
by removing the ":" colon in between the numbers, and replacing it with a Newline symbol "\n" I change it to become,
sprintf(timeStr, "%c%c\n%c%c", hoursChar[0],hoursChar[1],minutesChar[0], minutesChar[1]);
this gives it a nice wrapped text format in the top corner, and gives us some space to play with in the side, for things like Pictures and icons, which we will do next..
If you have been able to do these things, you now have completed the 2nd part of the tutorial, and now know how to change and modify the position of labels..
Using icons
Image size considerations
Preparing the image for inclusion as an icon
RGB565 image format
Flipping the bytes
RAM vs ROM
Using git to work on the firmware
Cloning the repository
Changing the code to add the image
Compiling the firmware
Testing the firmware
Installing the new firmware
A holistic guide on how to install different firmware using various hardware programmers is available here: Reprogramming the PineTime. There's also the possibility of using OTA. //TODO: add that to the article!